| package com.googlesource.gerrit.plugins.supermanifest; |
| |
| import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS; |
| |
| import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.text.MessageFormat; |
| import java.util.Date; |
| import java.util.StringJoiner; |
| import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.api.errors.JGitInternalException; |
| import org.eclipse.jgit.dircache.DirCache; |
| import org.eclipse.jgit.dircache.DirCacheBuilder; |
| import org.eclipse.jgit.dircache.DirCacheEntry; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.gitrepo.internal.RepoText; |
| import org.eclipse.jgit.internal.JGitText; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.FileMode; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.RefUpdate.Result; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| class JiriUpdater implements SubModuleUpdater { |
| PersonIdent serverIdent; |
| URI canonicalWebUrl; |
| |
| public JiriUpdater(PersonIdent serverIdent, URI canonicalWebUrl) { |
| this.serverIdent = serverIdent; |
| this.canonicalWebUrl = canonicalWebUrl; |
| } |
| |
| private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class); |
| |
| private void warn(String formatStr, Object... args) { |
| // The docs claim that log.warn() uses format strings, but it doesn't seem to work, so we do it |
| // explicitly. |
| log.warn(canonicalWebUrl + " : " + String.format(formatStr, args)); |
| } |
| |
| private void updateSubmodules( |
| Repository repo, |
| String targetRef, |
| URI targetURI, |
| JiriProjects projects, |
| GerritRemoteReader reader) |
| throws IOException, GitAPIException { |
| DirCache index = DirCache.newInCore(); |
| DirCacheBuilder builder = index.builder(); |
| ObjectInserter inserter = repo.newObjectInserter(); |
| try (RevWalk rw = new RevWalk(repo)) { |
| Config cfg = new Config(); |
| projects.sortByPath(); |
| String parent = null; |
| for (JiriProjects.Project proj : projects.getProjects()) { |
| String path = proj.getPath(); |
| String nameUri = proj.getRemote(); |
| if (parent != null) { |
| String p1 = StringUtil.stripAndAddCharsAtEnd(path, "/"); |
| String p2 = StringUtil.stripAndAddCharsAtEnd(parent, "/"); |
| if (p1.startsWith(p2)) { |
| warn( |
| "Skipping project %s(%s) as git doesn't support nested submodules", |
| proj.getName(), path); |
| continue; |
| } |
| } |
| |
| ObjectId objectId; |
| String ref = proj.getRef(); |
| |
| if (ObjectId.isId(ref)) { |
| objectId = ObjectId.fromString(ref); |
| } else { |
| objectId = reader.sha1(nameUri, ref); |
| if (objectId == null) { |
| warn("failed to get ref '%s' for '%s', skipping", ref, nameUri); |
| continue; |
| } |
| } |
| |
| // can be branch or tag |
| cfg.setString("submodule", path, "branch", ref); |
| |
| if (proj.getHistorydepth() > 0) { |
| cfg.setBoolean("submodule", path, "shallow", true); |
| if (proj.getHistorydepth() != 1) { |
| warn( |
| "Project %s(%s) has historydepth other than 1. Submodule only support shallow of depth 1.", |
| proj.getName(), proj.getPath()); |
| } |
| } |
| |
| URI submodUrl = URI.create(nameUri); |
| |
| // check if repo exists locally then relativize its URL |
| try { |
| String repoName = submodUrl.getPath(); |
| while (repoName.startsWith("/")) { |
| repoName = repoName.substring(1); |
| } |
| reader.openRepository(repoName); |
| submodUrl = relativize(targetURI, URI.create(repoName)); |
| } catch (RepositoryNotFoundException e) { |
| } |
| cfg.setString("submodule", path, "path", path); |
| cfg.setString("submodule", path, "url", submodUrl.toString()); |
| |
| // create gitlink |
| DirCacheEntry dcEntry = new DirCacheEntry(path); |
| dcEntry.setObjectId(objectId); |
| dcEntry.setFileMode(FileMode.GITLINK); |
| builder.add(dcEntry); |
| parent = path; |
| } |
| |
| String content = cfg.toText(); |
| |
| // create a new DirCacheEntry for .gitmodules file. |
| final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES); |
| ObjectId objectId = |
| inserter.insert(Constants.OBJ_BLOB, content.getBytes(Constants.CHARACTER_ENCODING)); |
| dcEntry.setObjectId(objectId); |
| dcEntry.setFileMode(FileMode.REGULAR_FILE); |
| builder.add(dcEntry); |
| |
| builder.finish(); |
| ObjectId treeId = index.writeTree(inserter); |
| |
| // Create a Commit object, populate it and write it |
| ObjectId headId = repo.resolve(targetRef + "^{commit}"); |
| CommitBuilder commit = new CommitBuilder(); |
| commit.setTreeId(treeId); |
| if (headId != null) commit.setParentIds(headId); |
| PersonIdent author = |
| new PersonIdent( |
| serverIdent.getName(), |
| serverIdent.getEmailAddress(), |
| new Date(), |
| serverIdent.getTimeZone()); |
| commit.setAuthor(author); |
| commit.setCommitter(author); |
| commit.setMessage(RepoText.get().repoCommitMessage); |
| |
| ObjectId commitId = inserter.insert(commit); |
| inserter.flush(); |
| |
| RefUpdate ru = repo.updateRef(targetRef); |
| ru.setNewObjectId(commitId); |
| ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId()); |
| Result rc = ru.update(rw); |
| |
| switch (rc) { |
| case NEW: |
| case FORCED: |
| case FAST_FORWARD: |
| // Successful. Do nothing. |
| break; |
| case REJECTED: |
| case LOCK_FAILURE: |
| throw new ConcurrentRefUpdateException( |
| MessageFormat.format(JGitText.get().cannotLock, targetRef), ru.getRef(), rc); |
| default: |
| throw new JGitInternalException( |
| MessageFormat.format( |
| JGitText.get().updatingRefFailed, targetRef, commitId.name(), rc)); |
| } |
| } |
| } |
| |
| private static final String SLASH = "/"; |
| /* |
| * Copied from https://github.com/eclipse/jgit/blob/e9fb111182b55cc82c530d82f13176c7a85cd958/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java#L729 |
| */ |
| static URI relativize(URI current, URI target) { |
| // We only handle bare paths for now. |
| if (!target.toString().equals(target.getPath())) { |
| return target; |
| } |
| if (!current.toString().equals(current.getPath())) { |
| return target; |
| } |
| |
| String cur = current.normalize().getPath(); |
| String dest = target.normalize().getPath(); |
| |
| if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) { |
| return target; |
| } |
| |
| while (cur.startsWith(SLASH)) { |
| cur = cur.substring(1); |
| } |
| while (dest.startsWith(SLASH)) { |
| dest = dest.substring(1); |
| } |
| |
| if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) { |
| // Avoid having to special-casing in the next two ifs. |
| String prefix = "prefix/"; |
| cur = prefix + cur; |
| dest = prefix + dest; |
| } |
| |
| if (!cur.endsWith(SLASH)) { |
| // The current file doesn't matter. |
| int lastSlash = cur.lastIndexOf('/'); |
| cur = cur.substring(0, lastSlash); |
| } |
| String destFile = ""; |
| if (!dest.endsWith(SLASH)) { |
| // We always have to provide the destination file. |
| int lastSlash = dest.lastIndexOf('/'); |
| destFile = dest.substring(lastSlash + 1, dest.length()); |
| dest = dest.substring(0, dest.lastIndexOf('/')); |
| } |
| |
| String[] cs = cur.split(SLASH); |
| String[] ds = dest.split(SLASH); |
| |
| int common = 0; |
| while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) { |
| common++; |
| } |
| |
| StringJoiner j = new StringJoiner(SLASH); |
| for (int i = common; i < cs.length; i++) { |
| j.add(".."); |
| } |
| for (int i = common; i < ds.length; i++) { |
| j.add(ds[i]); |
| } |
| |
| j.add(destFile); |
| return URI.create(j.toString()); |
| } |
| |
| @Override |
| public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef) |
| throws IOException, GitAPIException, ConfigInvalidException { |
| try (Repository destRepo = reader.openRepository(c.getDestRepoKey().toString())) { |
| JiriProjects projects = |
| JiriManifestParser.getProjects( |
| reader, c.getSrcRepoKey().toString(), srcRef, c.getXmlPath()); |
| String targetRef = c.getDestBranch().equals("*") ? srcRef : REFS_HEADS + c.getDestBranch(); |
| updateSubmodules( |
| destRepo, targetRef, URI.create(c.getDestRepoKey().toString() + "/"), projects, reader); |
| } |
| } |
| } |