Implement ReplicationConfigOverrides.update()

The newly added `update(Config)` method to `ReplicationConfigOverrides`
allows us to update the replication configuration programmatically.

Added implementation stores remotes definitions in the "fanout" fashion
in the `refs/meta/replication` branch of `All-Projects` repository.
Other configuration options will be stored in the `replication.config`
file in the root of the branch.

If no configuration or branch is present they will be created by the
script.

Change-Id: Ifa744342536fc029a220833f8f59c9a15954ac65
diff --git a/replication/replication-config-from-git.groovy b/replication/replication-config-from-git.groovy
index efcb32f..dfeb812 100644
--- a/replication/replication-config-from-git.groovy
+++ b/replication/replication-config-from-git.groovy
@@ -12,33 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import com.google.gerrit.common.Nullable
+import com.google.gerrit.server.update.context.RefUpdateContext
 import com.googlesource.gerrit.plugins.replication.*
-  import com.googlesource.gerrit.plugins.replication.FanoutConfigResource.*
-  import com.googlesource.gerrit.plugins.replication.FileConfigResource.*
 
-  import com.google.inject.*
   import com.google.common.collect.*
   import com.google.common.flogger.*
   import com.google.common.io.*
-  import com.google.common.io.Files.*
   import com.google.gerrit.entities.*
   import com.google.gerrit.extensions.registration.*
   import com.google.gerrit.server.config.*
   import com.google.gerrit.server.git.*
+  import com.google.gerrit.server.*
   import com.google.inject.*
 
-  import java.io.*
-  import java.util.*
-
   import org.eclipse.jgit.errors.*
+  import org.eclipse.jgit.dircache.*
   import org.eclipse.jgit.lib.*
-  import org.eclipse.jgit.lib.FileMode.*
 
   import org.eclipse.jgit.revwalk.*
   import org.eclipse.jgit.treewalk.*
 
-  class GitReplicationConfigOverrides implements ReplicationConfigOverrides {
-    FluentLogger logger = FluentLogger.forEnclosingClass()
+import static java.nio.charset.StandardCharsets.*
+import static org.eclipse.jgit.dircache.DirCacheEntry.*
+import static org.eclipse.jgit.lib.FileMode.*
+import static org.eclipse.jgit.lib.RefUpdate.Result.*
+
+class GitReplicationConfigOverrides implements ReplicationConfigOverrides {
+    static final FluentLogger logger = FluentLogger.forEnclosingClass()
     Config EMPTY_CONFIG = new Config()
 
     def REF_NAME = RefNames.REFS_META + "replication"
@@ -49,6 +50,13 @@
     @Inject
     GitRepositoryManager repoManager
 
+    @Inject
+    Provider<AllProjectsName> allProjectsNameProvider
+
+    @Inject
+    @GerritPersonIdent
+    PersonIdent gerritPersonIdent
+
     @Override
     Config getConfig() {
       Config config = EMPTY_CONFIG
@@ -86,7 +94,7 @@
 
         tw.enterSubtree()
         while (tw.next()) {
-          if (tw.fileMode == FileMode.REGULAR_FILE && tw.nameString.endsWith(".config")) {
+          if (tw.fileMode == REGULAR_FILE && tw.nameString.endsWith(".config")) {
             Config remoteConfig = new BlobBasedConfig(new Config(), repo, tw.getObjectId(0))
             addRemoteConfig(tw.nameString, remoteConfig, destination)
           }
@@ -96,7 +104,7 @@
       destination
     }
 
-    def removeRemotes(Config config) {
+    static def removeRemotes(Config config) {
       Set < String > remoteNames = config.getSubsections("remote")
       if (!remoteNames) {
         logger.atSevere().log(
@@ -109,7 +117,7 @@
       }
     }
 
-    def addRemoteConfig(String fileName, Config source, Config destination) {
+    static def addRemoteConfig(String fileName, Config source, Config destination) {
       String remoteName = Files.getNameWithoutExtension(fileName)
       source.getNames("remote").each {
         name ->
@@ -133,6 +141,142 @@
         repo.close()
       }
     }
+
+    @Override
+    void update(Config config) throws IOException {
+      Repository repo = repoManager.openRepository(allProjectsNameProvider.get())
+      RefUpdateContext ctx = RefUpdateContext.open(RefUpdateContext.RefUpdateType.PLUGIN)
+      RevWalk rw = new RevWalk(repo)
+      ObjectReader reader = repo.newObjectReader()
+      ObjectInserter inserter = repo.newObjectInserter()
+      try {
+        ObjectId configHead = repo.resolve(REF_NAME)
+        DirCache dirCache = readTree(repo, reader, configHead)
+        DirCacheEditor editor = dirCache.editor()
+        Config rootConfig = readConfig(FileConfigResource.CONFIG_NAME, repo, rw, configHead)
+
+        for (String section : config.getSections()) {
+          if ("remote".equals(section)) {
+            updateRemoteConfig(config, repo, rw, configHead, editor, inserter)
+          } else {
+            updateRootConfig(config, section, rootConfig)
+          }
+        }
+        insertConfig(FileConfigResource.CONFIG_NAME, rootConfig, editor, inserter)
+        editor.finish()
+
+        CommitBuilder cb = new CommitBuilder()
+        ObjectId newTreeId = dirCache.writeTree(inserter)
+        if (configHead != null) {
+          ObjectId oldTreeId = repo.parseCommit(configHead).tree
+          if (oldTreeId == newTreeId) {
+            logger.atInfo().log("No configuration changes were applied, ignoring")
+            return;
+          }
+          cb.setParentId(configHead)
+        }
+        cb.setAuthor(gerritPersonIdent)
+        cb.setCommitter(gerritPersonIdent)
+        cb.setTreeId(newTreeId);
+        cb.setMessage("Update configuration")
+        ObjectId newConfigHead = inserter.insert(cb)
+        inserter.flush()
+        RefUpdate refUpdate = repo.getRefDatabase().newUpdate(REF_NAME, false)
+        refUpdate.setNewObjectId(newConfigHead)
+        RefUpdate.Result result = refUpdate.update()
+        if (result != FAST_FORWARD && result != NEW) {
+          throw new IOException("Updating replication config failed: " + result)
+        }
+      } finally {
+        inserter.close()
+        reader.close()
+        rw.close()
+        ctx.close()
+        repo.close()
+      }
+    }
+
+     Config readConfig(
+        String configPath, Repository repo, RevWalk rw, @Nullable ObjectId treeId) {
+      if (treeId != null) {
+        try {
+          RevTree tree = rw.parseTree(treeId)
+          TreeWalk tw = TreeWalk.forPath(repo, configPath, tree)
+          if (tw != null) {
+            return new BlobBasedConfig(new Config(), repo, tw.getObjectId(0))
+          }
+        } catch (ConfigInvalidException | IOException e) {
+          logger.atWarning().withCause(e).log(
+              "failed to load replication configuration from branch %s of %s, path %s",
+              REF_NAME, allProjectsName.get(), configPath)
+        }
+      }
+
+      return new Config();
+    }
+
+    static DirCache readTree(Repository repo, ObjectReader reader, ObjectId configHead)
+        throws IOException {
+      DirCache dc = DirCache.newInCore()
+      if (configHead != null) {
+        RevTree tree = repo.parseCommit(configHead).getTree()
+        DirCacheBuilder b = dc.builder()
+        b.addTree(new byte[0], STAGE_0, reader, tree)
+        b.finish()
+      }
+      return dc;
+    }
+
+    void updateRemoteConfig(
+        Config config,
+        Repository repo,
+        RevWalk rw,
+        @Nullable ObjectId refId,
+        DirCacheEditor editor,
+        ObjectInserter inserter)
+        throws IOException {
+      for (String remoteName : config.getSubsections("remote")) {
+        String configPath = String.format("%s/%s.config", FanoutConfigResource.CONFIG_DIR, remoteName)
+        Config baseConfig = readConfig(configPath, repo, rw, refId)
+
+        updateConfigSubSections(config, "remote", remoteName, baseConfig)
+        insertConfig(configPath, baseConfig, editor, inserter)
+      }
+    }
+
+    static void updateRootConfig(Config config, String section, Config rootConfig) {
+      for (String subsection : config.getSubsections(section)) {
+        updateConfigSubSections(config, section, subsection, rootConfig)
+      }
+
+      for (String name : config.getNames(section, true)) {
+        List<String> values = Lists.newArrayList(config.getStringList(section, null, name))
+        rootConfig.setStringList(section, null, name, values)
+      }
+    }
+
+    static void updateConfigSubSections(
+        Config source, String section, String subsection, Config destination) {
+      for (String name : source.getNames(section, subsection, true)) {
+        List<String> values = Lists.newArrayList(source.getStringList(section, subsection, name))
+        destination.setStringList(section, subsection, name, values)
+      }
+    }
+
+    static void insertConfig(
+        String configPath, Config config, DirCacheEditor editor, ObjectInserter inserter)
+        throws IOException {
+      String configText = config.toText()
+      ObjectId configId = inserter.insert(Constants.OBJ_BLOB, configText.getBytes(UTF_8))
+      editor.add(
+          new DirCacheEditor.PathEdit(configPath) {
+            @Override
+            void apply(DirCacheEntry ent) {
+              ent.setFileMode(REGULAR_FILE)
+              ent.setObjectId(configId)
+            }
+          });
+    }
   }
 
 class GitReplicationConfigModule implements Module {