| // Copyright (C) 2024 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // 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.api.* | |
| import com.google.common.collect.* | |
| import com.google.common.flogger.* | |
| import com.google.common.io.* | |
| 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 org.eclipse.jgit.errors.* | |
| import org.eclipse.jgit.dircache.* | |
| import org.eclipse.jgit.lib.* | |
| import org.eclipse.jgit.revwalk.* | |
| import org.eclipse.jgit.treewalk.* | |
| 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 CONFIG_NAME = "replication.config" | |
| def CONFIG_DIR = "replication" | |
| def REF_NAME = RefNames.REFS_META + "replication" | |
| @Inject | |
| AllProjectsName allProjectsName | |
| @Inject | |
| GitRepositoryManager repoManager | |
| @Inject | |
| Provider<AllProjectsName> allProjectsNameProvider | |
| @Inject | |
| @GerritPersonIdent | |
| PersonIdent gerritPersonIdent | |
| @Override | |
| Config getConfig() { | |
| Config config = EMPTY_CONFIG | |
| Repository repo = repoManager.openRepository(allProjectsName) | |
| RevWalk rw = new RevWalk(repo) | |
| try { | |
| Ref ref = repo.exactRef(REF_NAME) | |
| if (ref) { | |
| RevTree tree = rw.parseTree(ref.objectId) | |
| config = addFanoutRemotes(repo, tree, getBaseConfig(repo, tree)) | |
| } | |
| } catch (IOException e) { | |
| logger.atWarning().withCause(e).log("Cannot read replication config from git repository") | |
| } catch (ConfigInvalidException e) { | |
| logger.atWarning().withCause(e).log("Cannot parse replication config from git repository") | |
| } finally { | |
| rw.close() | |
| repo.close() | |
| } | |
| config | |
| } | |
| Config getBaseConfig(Repository repo, RevTree tree) { | |
| TreeWalk tw = TreeWalk.forPath(repo, CONFIG_NAME, tree) | |
| return tw ? new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) : EMPTY_CONFIG | |
| } | |
| Config addFanoutRemotes(Repository repo, RevTree tree, Config destination) | |
| throws IOException, ConfigInvalidException { | |
| TreeWalk tw = TreeWalk.forPath(repo, CONFIG_DIR, tree) | |
| if (tw) { | |
| removeRemotes(destination) | |
| tw.enterSubtree() | |
| while (tw.next()) { | |
| if (tw.fileMode == REGULAR_FILE && tw.nameString.endsWith(".config")) { | |
| Config remoteConfig = new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) | |
| addRemoteConfig(tw.nameString, remoteConfig, destination) | |
| } | |
| } | |
| } | |
| destination | |
| } | |
| static def removeRemotes(Config config) { | |
| Set < String > remoteNames = config.getSubsections("remote") | |
| if (!remoteNames) { | |
| logger.atSevere().log( | |
| "When replication directory is present replication.config file cannot contain remote configuration. Ignoring: %s", | |
| remoteNames.join(",")) | |
| for (String name: remoteNames) { | |
| config.unsetSection("remote", name) | |
| } | |
| } | |
| } | |
| static def addRemoteConfig(String fileName, Config source, Config destination) { | |
| String remoteName = Files.getNameWithoutExtension(fileName) | |
| source.getNames("remote").each { | |
| name -> | |
| destination.setStringList( | |
| "remote", | |
| remoteName, | |
| name, | |
| Lists.newArrayList(source.getStringList("remote", null, name))) | |
| } | |
| } | |
| @Override | |
| String getVersion() { | |
| Repository repo = repoManager.openRepository(allProjectsName) | |
| try { | |
| ObjectId configHead = repo.resolve(REF_NAME) | |
| return configHead ? configHead.name() : "" | |
| } catch (IOException e) { | |
| throw new IllegalStateException("Could not open replication configuration repository", e) | |
| } finally { | |
| 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(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(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", 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 { | |
| @Override | |
| void configure(Binder binder) { | |
| DynamicItem.bind(binder, ReplicationConfigOverrides.class) | |
| .to(GitReplicationConfigOverrides.class) | |
| } | |
| } | |
| modules = [GitReplicationConfigModule] |