[project] Update project comparison between remote and local projects.

Use normalized URLs for comparing local and remote projects and
generating project keys. Fixes issues where remote and local manifest
use different schemes (sso:// vs https://).

Bug: 66891
Testing:
- Unit tests
- Verified repo case (see bug) doesn't try to re-checkout remote manifest.

Change-Id: I76b70fb8a44b70af258a1921a82aadd43b806634
Reviewed-on: https://fuchsia-review.googlesource.com/c/jiri/+/509563
Reviewed-by: Nathan Mulcahey <nmulcahey@google.com>
Commit-Queue: Ian Kasprzak <iankaz@google.com>
diff --git a/project/project.go b/project/project.go
index 205019f..92dd629 100644
--- a/project/project.go
+++ b/project/project.go
@@ -120,9 +120,9 @@
 // ProjectKey is a unique string for a project.
 type ProjectKey string
 
-// MakeProjectKey returns the project key, given the project name and remote.
+// MakeProjectKey returns the project key, given the project name and normalized remote.
 func MakeProjectKey(name, remote string) ProjectKey {
-	return ProjectKey(name + KeySeparator + remote)
+	return ProjectKey(name + KeySeparator + rewriteAndNormalizeRemote(remote))
 }
 
 // KeySeparator is a reserved string used in ProjectKeys and HookKeys.
@@ -899,6 +899,20 @@
 	return remote
 }
 
+// rewriteAndNormalizeRemote rewrites sso:// prefixed remotes and removes the
+// scheme (e.g. https://) from the remote.
+func rewriteAndNormalizeRemote(remote string) string {
+	if strings.HasPrefix(remote, "sso://") {
+		remote = ssoRe.ReplaceAllString(remote, "https://$1.googlesource.com/")
+	}
+	u, err := url.Parse(remote)
+	if err != nil {
+		// If remote isn't parseable don't try to remove schema
+		return remote
+	}
+	return strings.TrimPrefix(remote, u.Scheme)
+}
+
 // LocalProjects returns projects on the local filesystem.  If all projects in
 // the manifest exist locally and scanMode is set to FastScan, then only the
 // projects in the manifest that exist locally will be returned.  Otherwise, a
@@ -996,7 +1010,8 @@
 		if _, ok := localProjects[remoteKey]; !ok {
 			for localKey := range localKeysNotInRemote {
 				localProject := localProjects[localKey]
-				if localProject.Path == remoteProject.Path && (localProject.Name == remoteProject.Name || localProject.Remote == remoteProject.Remote) {
+				if localProject.Path == remoteProject.Path &&
+					(localProject.Name == remoteProject.Name || rewriteAndNormalizeRemote(localProject.Remote) == rewriteAndNormalizeRemote(remoteProject.Remote)) {
 					delete(localProjects, localKey)
 					delete(localKeysNotInRemote, localKey)
 					// Change local project key
@@ -1367,7 +1382,6 @@
 // removed.
 func UpdateUniverse(jirix *jiri.X, gc, localManifest, rebaseTracked, rebaseUntracked, rebaseAll, runHooks, fetchPkgs bool, runHookTimeout, fetchTimeout uint) (e error) {
 	jirix.Logger.Infof("Updating all projects")
-
 	updateFn := func(scanMode ScanMode) error {
 		jirix.TimerPush(fmt.Sprintf("update universe: %s", scanMode))
 		defer jirix.TimerPop()
@@ -1377,7 +1391,6 @@
 		if err != nil {
 			return err
 		}
-
 		// Determine the set of remote projects and match them up with the locals.
 		remoteProjects, hooks, pkgs, err := LoadUpdatedManifest(jirix, localProjects, localManifest)
 		MatchLocalWithRemote(localProjects, remoteProjects)
diff --git a/project/project_test.go b/project/project_test.go
index 8a6ee8c..862073d 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -145,6 +145,49 @@
 	}
 }
 
+// TestProjectKeysMatch tests that projects with same remote match when
+// accessed with different URL schemes.
+func TestProjectKeysMatch(t *testing.T) {
+	expected := project.Project{
+		Name:   "project1",
+		Remote: "https://fuchsia.googlesource.com/project1",
+	}
+	testProjectListsTrue := []project.Project{
+		{
+			Name:   "project1",
+			Remote: "persistent-https://fuchsia.googlesource.com/project1",
+		},
+		{
+			Name:   "project1",
+			Remote: "sso://fuchsia/project1",
+		},
+	}
+	testProjectListsFalse := []project.Project{
+		{
+			Name:   "project2",
+			Remote: "https://fuchsia.googlesource.com/project2",
+		},
+		{
+			Name:   "project1",
+			Remote: "https://fuchsia.googlesource.com/project2",
+		},
+		{
+			Name:   "project1",
+			Remote: "sso://fuchia/project2",
+		},
+	}
+	for _, item := range testProjectListsTrue {
+		if item.Key() != expected.Key() {
+			t.Errorf("expecting Key() to match between Projects %v and %v", expected, item)
+		}
+	}
+	for _, item := range testProjectListsFalse {
+		if item.Key() == expected.Key() {
+			t.Errorf("expecting Key() to not match between Projects %v and %v", expected, item)
+		}
+	}
+}
+
 // TestLocalProjects tests the behavior of the LocalProjects method with
 // different ScanModes.
 func TestLocalProjects(t *testing.T) {