Repository: gocd/gocd
Issue: Polling and builds fail when a git submodule URL changes

This causes `git submodule update --init` to fail which means material updates fail and builds running on the agent fail if they have the stale submodule url:

```
git submodule update --init
--- Environment ---
{}
--- INPUT ----


--OUTPUT ---
<cut>
ERROR: fatal: reference is not a tree: 176026c856dd4cecd80fcdd5fe904fbc9f4c1385
ERROR: Unable to checkout '176026c856dd4cecd80fcdd5fe904fbc9f4c1385' in submodule path 'src/gnatsd'
```

`git submodule sync` is needed to refresh the submodule url.

This is time consuming to recover from because the fix involves running git submodule sync on the flyweights on the server and all copies on the agents which have stale urls.


Changed files:
common/test/com/thoughtworks/go/helper/GitSubmoduleRepos.java, common/test/com/thoughtworks/go/domain/materials/git/GitCommandTest.java, common/src/com/thoughtworks/go/domain/materials/git/GitCommand.java

diff --git a/common/src/com/thoughtworks/go/domain/materials/git/GitCommand.java b/common/src/com/thoughtworks/go/domain/materials/git/GitCommand.java
index 65d377c49a..849470b65d 100644
--- a/common/src/com/thoughtworks/go/domain/materials/git/GitCommand.java
+++ b/common/src/com/thoughtworks/go/domain/materials/git/GitCommand.java
@@ -149,7 +149,7 @@ public class GitCommand extends SCMCommand {
         run(gitCmd, outputStreamConsumer);
     }
 
-    private void resetHard(ProcessOutputStreamConsumer outputStreamConsumer, Revision revision) {
+    public void resetHard(ProcessOutputStreamConsumer outputStreamConsumer, Revision revision) {
         outputStreamConsumer.stdOutput("[GIT] Updating working copy to revision " + revision.getRevision());
         String[] args = new String[]{"reset", "--hard", revision.getRevision()};
         CommandLine gitCmd = git().withArgs(args).withWorkingDir(workingDir);
@@ -172,9 +172,17 @@ public class GitCommand extends SCMCommand {
             return;
         }
         outputStreamConsumer.stdOutput("[GIT] Updating git sub-modules");
-        String[] args = new String[]{"submodule", "update", "--init"};
-        CommandLine gitCmd = git().withArgs(args).withWorkingDir(workingDir);
-        runOrBomb(gitCmd);
+
+        String[] initArgs = new String[]{"submodule", "init"};
+        CommandLine initCmd = git().withArgs(initArgs).withWorkingDir(workingDir);
+        runOrBomb(initCmd);
+
+        submoduleSync();
+
+        String[] updateArgs = new String[]{"submodule", "update"};
+        CommandLine updateCmd = git().withArgs(updateArgs).withWorkingDir(workingDir);
+        runOrBomb(updateCmd);
+
         outputStreamConsumer.stdOutput("[GIT] Cleaning unversioned files and sub-modules");
         printSubmoduleStatus(outputStreamConsumer);
     }
@@ -259,7 +267,7 @@ public class GitCommand extends SCMCommand {
         runOrBomb(gitCmd);
     }
 
-    private void fetch(ProcessOutputStreamConsumer outputStreamConsumer) {
+    public void fetch(ProcessOutputStreamConsumer outputStreamConsumer) {
         outputStreamConsumer.stdOutput("[GIT] Fetching changes");
         CommandLine gitFetch = git().withArgs("fetch", "origin").withWorkingDir(workingDir);
 
@@ -357,4 +365,20 @@ public class GitCommand extends SCMCommand {
         ConsoleResult consoleResult = runOrBomb(getCurrentBranchCommand);
         return consoleResult.outputAsString();
     }
+
+    public void changeSubmoduleUrl(String submoduleName, String newUrl) {
+        String[] args = new String[]{"config", "--file", ".gitmodules", "submodule." + submoduleName + ".url", newUrl};
+        CommandLine gitConfig = git().withArgs(args).withWorkingDir(workingDir);
+        runOrBomb(gitConfig);
+    }
+
+    public void submoduleSync() {
+        String[] syncArgs = new String[]{"submodule", "sync"};
+        CommandLine syncCmd = git().withArgs(syncArgs).withWorkingDir(workingDir);
+        runOrBomb(syncCmd);
+
+        String[] foreachArgs = new String[]{"submodule", "foreach", "--recursive", "git", "submodule", "sync"};
+        CommandLine foreachCmd = git().withArgs(foreachArgs).withWorkingDir(workingDir);
+        runOrBomb(foreachCmd);
+    }
 }
diff --git a/common/test/com/thoughtworks/go/domain/materials/git/GitCommandTest.java b/common/test/com/thoughtworks/go/domain/materials/git/GitCommandTest.java
index 7094807e96..36a7804cc2 100644
--- a/common/test/com/thoughtworks/go/domain/materials/git/GitCommandTest.java
+++ b/common/test/com/thoughtworks/go/domain/materials/git/GitCommandTest.java
@@ -445,6 +445,24 @@ public class GitCommandTest {
         clonedCopy.fetchAndReset(outputStreamConsumer, new StringRevision("HEAD"));
     }
 
+    @Test
+    public void shouldAllowSubmoduleUrlstoChange() throws Exception {
+        InMemoryStreamConsumer outputStreamConsumer = inMemoryConsumer();
+        GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos();
+        String submoduleDirectoryName = "local-submodule";
+        File cloneDirectory = createTempWorkingDirectory();
+
+        File remoteSubmoduleLocation = submoduleRepos.addSubmodule(SUBMODULE, submoduleDirectoryName);
+
+        GitCommand clonedCopy = new GitCommand(null, cloneDirectory, GitMaterialConfig.DEFAULT_BRANCH, false);
+        clonedCopy.cloneFrom(outputStreamConsumer, submoduleRepos.mainRepo().getUrl());
+        clonedCopy.fetchAndResetToHead(outputStreamConsumer);
+
+        submoduleRepos.changeSubmoduleUrl(submoduleDirectoryName);
+
+        clonedCopy.fetchAndResetToHead(outputStreamConsumer);
+    }
+
     private List<File> allFilesIn(File directory, String prefixOfFiles) {
         return new ArrayList<File>(FileUtils.listFiles(directory, andFileFilter(fileFileFilter(), prefixFileFilter(prefixOfFiles)), null));
     }
diff --git a/common/test/com/thoughtworks/go/helper/GitSubmoduleRepos.java b/common/test/com/thoughtworks/go/helper/GitSubmoduleRepos.java
index 6f4787a81b..423ad2fa4f 100644
--- a/common/test/com/thoughtworks/go/helper/GitSubmoduleRepos.java
+++ b/common/test/com/thoughtworks/go/helper/GitSubmoduleRepos.java
@@ -21,6 +21,7 @@ import com.thoughtworks.go.config.materials.git.GitMaterialConfig;
 import com.thoughtworks.go.domain.materials.Modification;
 import com.thoughtworks.go.domain.materials.TestSubprocessExecutionContext;
 import com.thoughtworks.go.domain.materials.git.GitCommand;
+import com.thoughtworks.go.domain.materials.mercurial.StringRevision;
 import com.thoughtworks.go.util.TestFileUtil;
 import org.apache.commons.io.FileUtils;
 
@@ -137,4 +138,18 @@ public class GitSubmoduleRepos extends TestRepo {
     @Override public GitMaterial material() {
         return new GitMaterial(remoteRepoDir.getAbsolutePath());
     }
+
+    public void changeSubmoduleUrl(String submoduleName) throws Exception {
+        File newSubmodule = createRepo("new-submodule");
+        addAndCommitNewFile(newSubmodule, "new", "make a commit");
+
+        git(remoteRepoDir).changeSubmoduleUrl(submoduleName, newSubmodule.getAbsolutePath());
+        git(remoteRepoDir).submoduleSync();
+
+        git(new File(remoteRepoDir, "local-submodule")).fetch(inMemoryConsumer());
+        git(new File(remoteRepoDir, "local-submodule")).resetHard(inMemoryConsumer(), new StringRevision("origin/master"));
+        git(remoteRepoDir).add(new File(".gitmodules"));
+        git(remoteRepoDir).add(new File("local-submodule"));
+        git(remoteRepoDir).commit("change submodule url");
+    }
 }