Add support for changing remote URL

Various git behaviour which can arise due to this are:
1. New remote doesn't have one of the tracking remote branch
   Behaviour: Git complains that upstream is gone when you checkout old
   branch
2. New remote had tracking branch A, which old remote also have but with
   a different tree
   Behaviour: Git complains that local branch has diverged from the
   remote branch

Change-Id: Ia5072980486df2d8feb2279d9d026bc8fde193e5
diff --git a/gitutil/git.go b/gitutil/git.go
index f83aab0..ff5e536 100644
--- a/gitutil/git.go
+++ b/gitutil/git.go
@@ -119,6 +119,13 @@
 	return g.run("remote", "add", name, path)
 }
 
+// GetRemoteBranchesContaining returns a slice of the remote branches
+// which contains the given commit
+func (g *Git) GetRemoteBranchesContaining(commit string) ([]string, error) {
+	branches, _, err := g.GetBranches("-r", "--contains", commit)
+	return branches, err
+}
+
 // BranchesDiffer tests whether two branches have any changes between them.
 func (g *Git) BranchesDiffer(branch1, branch2 string) (bool, error) {
 	out, err := g.runOutput("--no-pager", "diff", "--name-only", branch1+".."+branch2)
@@ -883,6 +890,11 @@
 	return g.run("remote", "set-url", name, url)
 }
 
+// DeleteRemote deletes the named remote
+func (g *Git) DeleteRemote(name string) error {
+	return g.run("remote", "rm", name)
+}
+
 // Stash attempts to stash any unsaved changes. It returns true if
 // anything was actually stashed, otherwise false. An error is
 // returned if the stash command fails.
diff --git a/project/project.go b/project/project.go
index de39df0..1c76e59 100644
--- a/project/project.go
+++ b/project/project.go
@@ -1061,8 +1061,7 @@
 		if _, ok := localProjects[remoteKey]; !ok {
 			for localKey, _ := range localKeysNotInRemote {
 				localProject := localProjects[localKey]
-				// Also do matching for name when we support remote rename
-				if localProject.Remote == remoteProject.Remote && localProject.Path == remoteProject.Path {
+				if localProject.Path == remoteProject.Path && (localProject.Name == remoteProject.Name || localProject.Remote == remoteProject.Remote) {
 					delete(localProjects, localKey)
 					delete(localKeysNotInRemote, localKey)
 					// Change local project key
@@ -2018,9 +2017,10 @@
 		}()
 	}
 
-	for key, _ := range localProjects {
+	for key, local := range localProjects {
 		remote, ok := remoteProjects[key]
-		if !ok || remote.Revision != "HEAD" {
+		// Don't update when project has pinned revision or it's remote has changed
+		if !ok || remote.Revision != "HEAD" || local.Remote != remote.Remote {
 			continue
 		}
 		keys <- key
@@ -2144,6 +2144,10 @@
 				jirix.Logger.Warningf("Not updating remotes for project %s(%s) due to its local-config\n\n", project.Name, project.Path)
 				continue
 			}
+			// Don't fetch when remote url has changed as that may cause fetch to fail
+			if r.Remote != project.Remote {
+				continue
+			}
 			wg.Add(1)
 			fetchLimit <- struct{}{}
 			project.HistoryDepth = r.HistoryDepth
@@ -2393,6 +2397,7 @@
 
 	ops := computeOperations(localProjects, remoteProjects, states, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot)
 	moveOperations := []moveOperation{}
+	changeRemoteOperations := operations{}
 	deleteOperations := []deleteOperation{}
 	updateOperations := operations{}
 	createOperations := []createOperation{}
@@ -2405,6 +2410,8 @@
 		switch o := op.(type) {
 		case deleteOperation:
 			deleteOperations = append(deleteOperations, o)
+		case changeRemoteOperation:
+			changeRemoteOperations = append(changeRemoteOperations, o)
 		case moveOperation:
 			moveOperations = append(moveOperations, o)
 		case updateOperation:
@@ -2418,6 +2425,9 @@
 	if err := runDeleteOperations(jirix, deleteOperations); err != nil {
 		return err
 	}
+	if err := runCommonOperations(jirix, changeRemoteOperations); err != nil {
+		return err
+	}
 	if err := runMoveOperations(jirix, moveOperations); err != nil {
 		return err
 	}
@@ -3044,6 +3054,81 @@
 	return nil
 }
 
+// changeRemoteOperation represents the chnage of remote URL
+type changeRemoteOperation struct {
+	commonOperation
+	rebaseTracked   bool
+	rebaseUntracked bool
+	rebaseAll       bool
+	snapshot        bool
+}
+
+func (op changeRemoteOperation) Kind() string {
+	return "change-remote"
+}
+
+func (op changeRemoteOperation) Run(jirix *jiri.X) error {
+	if op.project.LocalConfig.Ignore || op.project.LocalConfig.NoUpdate {
+		jirix.Logger.Warningf("Project %s(%s) won't be updated due to it's local-config. It has a changed remote\n\n", op.project.Name, op.project.Path)
+		return nil
+	}
+	git := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
+	tempRemote := "new-remote-origin"
+	if err := git.AddRemote(tempRemote, op.project.Remote); err != nil {
+		return err
+	}
+	defer git.DeleteRemote(tempRemote)
+
+	if err := fetch(jirix, op.project.Path, tempRemote); err != nil {
+		return err
+	}
+
+	// Check for all leaf commits in new remote
+	for _, branch := range op.state.Branches {
+		if containingBranches, err := git.GetRemoteBranchesContaining(branch.Revision); err != nil {
+			return err
+		} else {
+			foundBranch := false
+			for _, remoteBranchName := range containingBranches {
+				if strings.HasPrefix(remoteBranchName, tempRemote) {
+					foundBranch = true
+					break
+				}
+			}
+			if !foundBranch {
+				jirix.Logger.Errorf("Note: For project %q(%v), remote url has changed. Its branch %q is on a commit", op.project.Name, op.project.Path, branch.Name)
+				jirix.Logger.Errorf("which is not in new remote(%v). Please manually reset your branches or move", op.project.Remote)
+				jirix.Logger.Errorf("your project folder out of the root and try again")
+				return nil
+			}
+
+		}
+	}
+
+	// Everything ok, change the remote url
+	if err := git.SetRemoteUrl("origin", op.project.Remote); err != nil {
+		return err
+	}
+
+	if err := fetch(jirix, op.project.Path, "", gitutil.AllOpt(true), gitutil.PruneOpt(true)); err != nil {
+		return err
+	}
+
+	if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil {
+		return err
+	}
+
+	return writeMetadata(jirix, op.project, op.project.Path)
+}
+
+func (op changeRemoteOperation) String() string {
+	return fmt.Sprintf("Change remote of project %q to %q and update it to %q", op.project.Name, op.project.Remote, fmtRevision(op.project.Revision))
+}
+
+func (op changeRemoteOperation) Test(jirix *jiri.X, _ *fsUpdates) error {
+	return nil
+}
+
 // updateOperation represents the update of a project.
 type updateOperation struct {
 	commonOperation
@@ -3108,7 +3193,7 @@
 // The order in which operation types are defined determines the order
 // in which operations are performed. For correctness and also to
 // minimize the chance of a conflict, the delete operations should
-// happen before move operations, which should happen before create
+// happen before change-remote operations, which should happen before move
 // operations. If two create operations make nested directories, the
 // outermost should be created first.
 func (ops operations) Less(i, j int) bool {
@@ -3117,14 +3202,16 @@
 		switch op.Kind() {
 		case "delete":
 			vals[idx] = 0
-		case "move":
+		case "change-remote":
 			vals[idx] = 1
-		case "update":
+		case "move":
 			vals[idx] = 2
 		case "create":
 			vals[idx] = 3
-		case "null":
+		case "update":
 			vals[idx] = 4
+		case "null":
+			vals[idx] = 5
 		}
 	}
 	if vals[0] != vals[1] {
@@ -3217,6 +3304,13 @@
 			}
 		}
 		switch {
+		case local.Remote != remote.Remote:
+			return changeRemoteOperation{commonOperation{
+				destination: remote.Path,
+				project:     *remote,
+				source:      local.Path,
+				state:       *state,
+			}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
 		case local.Path != remote.Path:
 			// moveOperation also does an update, so we don't need to check the
 			// revision here.
diff --git a/project/project_test.go b/project/project_test.go
index c9dfc0f..e424960 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -1213,6 +1213,45 @@
 	checkReadme(t, fake.X, localProjects[1], "initial readme")
 }
 
+// TestUpdateUniverseChangeRemote checks that UpdateUniverse can change remote
+// of a project.
+func TestUpdateUniverseChangeRemote(t *testing.T) {
+	localProjects, fake, cleanup := setupUniverse(t)
+	defer cleanup()
+	if err := fake.UpdateUniverse(false); err != nil {
+		t.Fatal(err)
+	}
+
+	changedRemoteDir := fake.Projects[localProjects[1].Name] + "-remote-changed"
+	if err := os.Rename(fake.Projects[localProjects[1].Name], changedRemoteDir); err != nil {
+		t.Fatal(err)
+	}
+
+	writeReadme(t, fake.X, changedRemoteDir, "new commit")
+
+	// Update the local path at which project 1 is located.
+	m, err := fake.ReadRemoteManifest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	projects := []project.Project{}
+	for _, p := range m.Projects {
+		if p.Name == localProjects[1].Name {
+			p.Remote = changedRemoteDir
+		}
+		projects = append(projects, p)
+	}
+	m.Projects = projects
+	if err := fake.WriteRemoteManifest(m); err != nil {
+		t.Fatal(err)
+	}
+	// Check that UpdateUniverse() moves the local copy of the project 1.
+	if err := fake.UpdateUniverse(false); err != nil {
+		t.Fatal(err)
+	}
+	checkReadme(t, fake.X, localProjects[1], "new commit")
+}
+
 func TestIgnoredProjectsNotMoved(t *testing.T) {
 	localProjects, fake, cleanup := setupUniverse(t)
 	defer cleanup()