diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java new file mode 100644 index 000000000..d52292dc9 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.BareWriterConfig; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class BareSuperprojectWriterTest extends RepositoryTestCase { + + private static final String SHA1_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + @Override + public void setUp() throws Exception { + super.setUp(); + } + + @Test + public void write_setGitModulesContents() throws Exception { + try (Repository bareRepo = createBareRepository()) { + RepoProject repoProject = new RepoProject("subprojectX", "path/to", + "refs/heads/branch-x", "remote", ""); + repoProject.setUrl("http://example.com/a"); + + RemoteReader mockRemoteReader = mock(RemoteReader.class); + when(mockRemoteReader.sha1("http://example.com/a", + "refs/heads/branch-x")) + .thenReturn(ObjectId.fromString(SHA1_A)); + + BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo, + null, "refs/heads/master", author, mockRemoteReader, + BareWriterConfig.getDefault()); + + RevCommit commit = w.write(Arrays.asList(repoProject)); + + String contents = readContents(bareRepo, commit, ".gitmodules"); + List contentLines = Arrays + .asList(contents.split("\n")); + assertThat(contentLines.get(0), + is("[submodule \"subprojectX\"]")); + assertThat(contentLines.subList(1, contentLines.size()), + containsInAnyOrder(is("\tbranch = refs/heads/branch-x"), + is("\tpath = path/to"), + is("\turl = http://example.com/a"))); + } + } + + private String readContents(Repository repo, RevCommit commit, + String path) throws Exception { + String idStr = commit.getId().name() + ":" + path; + ObjectId modId = repo.resolve(idStr); + try (ObjectReader reader = repo.newObjectReader()) { + return new String( + reader.open(modId).getCachedBytes(Integer.MAX_VALUE)); + + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java new file mode 100644 index 000000000..d6482968f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.List; + +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.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteUnavailableException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +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.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.FileUtils; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a bare + * repository. + * + * To write on a regular repository, see {@link RegularSuperprojectWriter}. + */ +class BareSuperprojectWriter { + private static final int LOCK_FAILURE_MAX_RETRIES = 5; + + // Retry exponentially with delays in this range + private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; + + private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; + + private final Repository repo; + + private final URI targetUri; + + private final String targetBranch; + + private final RemoteReader callback; + + private final BareWriterConfig config; + + private final PersonIdent author; + + static class BareWriterConfig { + boolean ignoreRemoteFailures = false; + + boolean recordRemoteBranch = true; + + boolean recordSubmoduleLabels = true; + + boolean recordShallowSubmodules = true; + + static BareWriterConfig getDefault() { + return new BareWriterConfig(); + } + + private BareWriterConfig() { + } + } + + BareSuperprojectWriter(Repository repo, URI targetUri, + String targetBranch, + PersonIdent author, RemoteReader callback, + BareWriterConfig config) { + assert (repo.isBare()); + this.repo = repo; + this.targetUri = targetUri; + this.targetBranch = targetBranch; + this.author = author; + this.callback = callback; + this.config = config; + } + + RevCommit write(List repoProjects) + throws GitAPIException { + DirCache index = DirCache.newInCore(); + ObjectInserter inserter = repo.newObjectInserter(); + + try (RevWalk rw = new RevWalk(repo)) { + prepareIndex(repoProjects, index, inserter); + ObjectId treeId = index.writeTree(inserter); + long prevDelay = 0; + for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { + try { + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (ConcurrentRefUpdateException e) { + prevDelay = FileUtils.delay(prevDelay, + LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, + LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); + Thread.sleep(prevDelay); + repo.getRefDatabase().refresh(); + } + } + // In the last try, just propagate the exceptions + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (IOException | InterruptedException e) { + throw new ManifestErrorException(e); + } + } + + private void prepareIndex(List projects, DirCache index, + ObjectInserter inserter) throws IOException, GitAPIException { + Config cfg = new Config(); + StringBuilder attributes = new StringBuilder(); + DirCacheBuilder builder = index.builder(); + for (RepoProject proj : projects) { + String name = proj.getName(); + String path = proj.getPath(); + String url = proj.getUrl(); + ObjectId objectId; + if (ObjectId.isId(proj.getRevision())) { + objectId = ObjectId.fromString(proj.getRevision()); + } else { + objectId = callback.sha1(url, proj.getRevision()); + if (objectId == null && !config.ignoreRemoteFailures) { + throw new RemoteUnavailableException(url); + } + if (config.recordRemoteBranch) { + // "branch" field is only for non-tag references. + // Keep tags in "ref" field as hint for other tools. + String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ + : "branch"; //$NON-NLS-1$ + cfg.setString("submodule", name, field, //$NON-NLS-1$ + proj.getRevision()); + } + + if (config.recordShallowSubmodules + && proj.getRecommendShallow() != null) { + // The shallow recommendation is losing information. + // As the repo manifests stores the recommended + // depth in the 'clone-depth' field, while + // git core only uses a binary 'shallow = true/false' + // hint, we'll map any depth to 'shallow = true' + cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ + true); + } + } + if (config.recordSubmoduleLabels) { + StringBuilder rec = new StringBuilder(); + rec.append("/"); //$NON-NLS-1$ + rec.append(path); + for (String group : proj.getGroups()) { + rec.append(" "); //$NON-NLS-1$ + rec.append(group); + } + rec.append("\n"); //$NON-NLS-1$ + attributes.append(rec.toString()); + } + + URI submodUrl = URI.create(url); + if (targetUri != null) { + submodUrl = RepoCommand.relativize(targetUri, submodUrl); + } + cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ + cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ + submodUrl.toString()); + + // create gitlink + if (objectId != null) { + DirCacheEntry dcEntry = new DirCacheEntry(path); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.GITLINK); + builder.add(dcEntry); + + for (CopyFile copyfile : proj.getCopyFiles()) { + RemoteFile rf = callback.readFileWithMode(url, + proj.getRevision(), copyfile.src); + objectId = inserter.insert(Constants.OBJ_BLOB, + rf.getContents()); + dcEntry = new DirCacheEntry(copyfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(rf.getFileMode()); + builder.add(dcEntry); + } + for (LinkFile linkfile : proj.getLinkFiles()) { + String link; + if (linkfile.dest.contains("/")) { //$NON-NLS-1$ + link = FileUtils.relativizeGitPath( + linkfile.dest.substring(0, + linkfile.dest.lastIndexOf('/')), + proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ + } else { + link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ + } + + objectId = inserter.insert(Constants.OBJ_BLOB, + link.getBytes(UTF_8)); + dcEntry = new DirCacheEntry(linkfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.SYMLINK); + builder.add(dcEntry); + } + } + } + String content = cfg.toText(); + + // create a new DirCacheEntry for .gitmodules file. + DirCacheEntry dcEntry = new DirCacheEntry( + Constants.DOT_GIT_MODULES); + ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, + content.getBytes(UTF_8)); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntry); + + if (config.recordSubmoduleLabels) { + // create a new DirCacheEntry for .gitattributes file. + DirCacheEntry dcEntryAttr = new DirCacheEntry( + Constants.DOT_GIT_ATTRIBUTES); + ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, + attributes.toString().getBytes(UTF_8)); + dcEntryAttr.setObjectId(attrId); + dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntryAttr); + } + + builder.finish(); + } + + private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, + RevWalk rw, ObjectId treeId) + throws IOException, ConcurrentRefUpdateException { + ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ + if (headId != null + && rw.parseCommit(headId).getTree().getId().equals(treeId)) { + // No change. Do nothing. + return rw.parseCommit(headId); + } + + CommitBuilder commit = new CommitBuilder(); + commit.setTreeId(treeId); + if (headId != null) { + commit.setParentIds(headId); + } + commit.setAuthor(author); + commit.setCommitter(author); + commit.setMessage(RepoText.get().repoCommitMessage); + + ObjectId commitId = inserter.insert(commit); + inserter.flush(); + + RefUpdate ru = repo.updateRef(targetBranch); + 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, targetBranch), ru.getRef(), rc); + default: + throw new JGitInternalException( + MessageFormat.format(JGitText.get().updatingRefFailed, + targetBranch, commitId.name(), rc)); + } + + return rw.parseCommit(commitId); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java new file mode 100644 index 000000000..afab9943a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.SubmoduleAddCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +import org.eclipse.jgit.gitrepo.internal.RepoText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a regular + * repository (using git submodule commands) + * + * To write on a bare repository, use {@link BareSuperprojectWriter} + */ +class RegularSuperprojectWriter { + + private Repository repo; + + private ProgressMonitor monitor; + + RegularSuperprojectWriter(Repository repo, ProgressMonitor monitor) { + this.repo = repo; + this.monitor = monitor; + } + + RevCommit write(List repoProjects) + throws GitAPIException { + try (Git git = new Git(repo)) { + for (RepoProject proj : repoProjects) { + addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), + proj.getRevision(), proj.getCopyFiles(), + proj.getLinkFiles(), git); + } + return git.commit().setMessage(RepoText.get().repoCommitMessage) + .call(); + } catch (IOException e) { + throw new ManifestErrorException(e); + } + } + + private void addSubmodule(String name, String url, String path, + String revision, List copyfiles, List linkfiles, + Git git) throws GitAPIException, IOException { + assert (!repo.isBare()); + assert (git != null); + if (!linkfiles.isEmpty()) { + throw new UnsupportedOperationException( + JGitText.get().nonBareLinkFilesNotSupported); + } + + SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) + .setURI(url); + if (monitor != null) { + add.setProgressMonitor(monitor); + } + + Repository subRepo = add.call(); + if (revision != null) { + try (Git sub = new Git(subRepo)) { + sub.checkout().setName(findRef(revision, subRepo)).call(); + } + subRepo.close(); + git.add().addFilepattern(path).call(); + } + for (CopyFile copyfile : copyfiles) { + copyfile.copy(); + git.add().addFilepattern(copyfile.dest).call(); + } + } + + private static String findRef(String ref, Repository repo) + throws IOException { + if (!ObjectId.isId(ref)) { + Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ + if (r != null) { + return r.getName(); + } + } + return ref; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java index e0a822479..30b862673 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -9,11 +9,6 @@ */ package org.eclipse.jgit.gitrepo; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; -import static org.eclipse.jgit.lib.Constants.R_REMOTES; -import static org.eclipse.jgit.lib.Constants.R_TAGS; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -31,34 +26,20 @@ import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.GitCommand; -import org.eclipse.jgit.api.SubmoduleAddCommand; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRefNameException; -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.gitrepo.ManifestParser.IncludedFileReader; -import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; -import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; 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.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FileUtils; @@ -80,12 +61,7 @@ * @since 3.4 */ public class RepoCommand extends GitCommand { - private static final int LOCK_FAILURE_MAX_RETRIES = 5; - // Retry exponentially with delays in this range - private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; - - private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; private String manifestPath; private String baseUri; @@ -93,14 +69,13 @@ public class RepoCommand extends GitCommand { private String groupsParam; private String branch; private String targetBranch = Constants.HEAD; - private boolean recordRemoteBranch = true; - private boolean recordSubmoduleLabels = true; - private boolean recordShallowSubmodules = true; private PersonIdent author; private RemoteReader callback; private InputStream inputStream; private IncludedFileReader includedReader; - private boolean ignoreRemoteFailures = false; + + private BareSuperprojectWriter.BareWriterConfig bareWriterConfig = BareSuperprojectWriter.BareWriterConfig + .getDefault(); private ProgressMonitor monitor; @@ -269,14 +244,14 @@ public RemoteFile readFileWithMode(String uri, String ref, String path) } @SuppressWarnings("serial") - private static class ManifestErrorException extends GitAPIException { + static class ManifestErrorException extends GitAPIException { ManifestErrorException(Throwable cause) { super(RepoText.get().invalidManifest, cause); } } @SuppressWarnings("serial") - private static class RemoteUnavailableException extends GitAPIException { + static class RemoteUnavailableException extends GitAPIException { RemoteUnavailableException(String uri) { super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri)); } @@ -421,7 +396,7 @@ public RepoCommand setTargetBranch(String branch) { * @since 4.2 */ public RepoCommand setRecordRemoteBranch(boolean enable) { - this.recordRemoteBranch = enable; + this.bareWriterConfig.recordRemoteBranch = enable; return this; } @@ -436,7 +411,7 @@ public RepoCommand setRecordRemoteBranch(boolean enable) { * @since 4.4 */ public RepoCommand setRecordSubmoduleLabels(boolean enable) { - this.recordSubmoduleLabels = enable; + this.bareWriterConfig.recordSubmoduleLabels = enable; return this; } @@ -451,7 +426,7 @@ public RepoCommand setRecordSubmoduleLabels(boolean enable) { * @since 4.4 */ public RepoCommand setRecommendShallow(boolean enable) { - this.recordShallowSubmodules = enable; + this.bareWriterConfig.recordShallowSubmodules = enable; return this; } @@ -485,7 +460,7 @@ public RepoCommand setProgressMonitor(ProgressMonitor monitor) { * @since 4.3 */ public RepoCommand setIgnoreRemoteFailures(boolean ignore) { - this.ignoreRemoteFailures = ignore; + this.bareWriterConfig.ignoreRemoteFailures = ignore; return this; } @@ -570,240 +545,18 @@ public RevCommit call() throws GitAPIException { } if (repo.isBare()) { - if (author == null) - author = new PersonIdent(repo); - if (callback == null) - callback = new DefaultRemoteReader(); List renamedProjects = renameProjects(filteredProjects); - - DirCache index = DirCache.newInCore(); - ObjectInserter inserter = repo.newObjectInserter(); - - try (RevWalk rw = new RevWalk(repo)) { - prepareIndex(renamedProjects, index, inserter); - ObjectId treeId = index.writeTree(inserter); - long prevDelay = 0; - for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { - try { - return commitTreeOnCurrentTip( - inserter, rw, treeId); - } catch (ConcurrentRefUpdateException e) { - prevDelay = FileUtils.delay(prevDelay, - LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, - LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); - Thread.sleep(prevDelay); - repo.getRefDatabase().refresh(); - } - } - // In the last try, just propagate the exceptions - return commitTreeOnCurrentTip(inserter, rw, treeId); - } catch (IOException | InterruptedException e) { - throw new ManifestErrorException(e); - } - } - try (Git git = new Git(repo)) { - for (RepoProject proj : filteredProjects) { - addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), - proj.getRevision(), proj.getCopyFiles(), - proj.getLinkFiles(), git); - } - return git.commit().setMessage(RepoText.get().repoCommitMessage) - .call(); - } catch (IOException e) { - throw new ManifestErrorException(e); - } - } - - private void prepareIndex(List projects, DirCache index, - ObjectInserter inserter) throws IOException, GitAPIException { - Config cfg = new Config(); - StringBuilder attributes = new StringBuilder(); - DirCacheBuilder builder = index.builder(); - for (RepoProject proj : projects) { - String name = proj.getName(); - String path = proj.getPath(); - String url = proj.getUrl(); - ObjectId objectId; - if (ObjectId.isId(proj.getRevision())) { - objectId = ObjectId.fromString(proj.getRevision()); - } else { - objectId = callback.sha1(url, proj.getRevision()); - if (objectId == null && !ignoreRemoteFailures) { - throw new RemoteUnavailableException(url); - } - if (recordRemoteBranch) { - // "branch" field is only for non-tag references. - // Keep tags in "ref" field as hint for other tools. - String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ - : "branch"; //$NON-NLS-1$ - cfg.setString("submodule", name, field, //$NON-NLS-1$ - proj.getRevision()); - } - - if (recordShallowSubmodules - && proj.getRecommendShallow() != null) { - // The shallow recommendation is losing information. - // As the repo manifests stores the recommended - // depth in the 'clone-depth' field, while - // git core only uses a binary 'shallow = true/false' - // hint, we'll map any depth to 'shallow = true' - cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ - true); - } - } - if (recordSubmoduleLabels) { - StringBuilder rec = new StringBuilder(); - rec.append("/"); //$NON-NLS-1$ - rec.append(path); - for (String group : proj.getGroups()) { - rec.append(" "); //$NON-NLS-1$ - rec.append(group); - } - rec.append("\n"); //$NON-NLS-1$ - attributes.append(rec.toString()); - } - - URI submodUrl = URI.create(url); - if (targetUri != null) { - submodUrl = relativize(targetUri, submodUrl); - } - cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ - cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ - submodUrl.toString()); - - // create gitlink - if (objectId != null) { - DirCacheEntry dcEntry = new DirCacheEntry(path); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.GITLINK); - builder.add(dcEntry); - - for (CopyFile copyfile : proj.getCopyFiles()) { - RemoteFile rf = callback.readFileWithMode(url, - proj.getRevision(), copyfile.src); - objectId = inserter.insert(Constants.OBJ_BLOB, - rf.getContents()); - dcEntry = new DirCacheEntry(copyfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(rf.getFileMode()); - builder.add(dcEntry); - } - for (LinkFile linkfile : proj.getLinkFiles()) { - String link; - if (linkfile.dest.contains("/")) { //$NON-NLS-1$ - link = FileUtils.relativizeGitPath( - linkfile.dest.substring(0, - linkfile.dest.lastIndexOf('/')), - proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ - } else { - link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ - } - - objectId = inserter.insert(Constants.OBJ_BLOB, - link.getBytes(UTF_8)); - dcEntry = new DirCacheEntry(linkfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.SYMLINK); - builder.add(dcEntry); - } - } - } - String content = cfg.toText(); - - // create a new DirCacheEntry for .gitmodules file. - DirCacheEntry dcEntry = new DirCacheEntry( - Constants.DOT_GIT_MODULES); - ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, - content.getBytes(UTF_8)); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntry); - - if (recordSubmoduleLabels) { - // create a new DirCacheEntry for .gitattributes file. - DirCacheEntry dcEntryAttr = new DirCacheEntry( - Constants.DOT_GIT_ATTRIBUTES); - ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, - attributes.toString().getBytes(UTF_8)); - dcEntryAttr.setObjectId(attrId); - dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntryAttr); + BareSuperprojectWriter writer = new BareSuperprojectWriter(repo, targetUri, + targetBranch, + author == null ? new PersonIdent(repo) : author, + callback == null ? new DefaultRemoteReader() : callback, + bareWriterConfig); + return writer.write(renamedProjects); } - builder.finish(); - } - private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, - RevWalk rw, ObjectId treeId) - throws IOException, ConcurrentRefUpdateException { - ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ - if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) { - // No change. Do nothing. - return rw.parseCommit(headId); - } - - CommitBuilder commit = new CommitBuilder(); - commit.setTreeId(treeId); - if (headId != null) - commit.setParentIds(headId); - commit.setAuthor(author); - commit.setCommitter(author); - commit.setMessage(RepoText.get().repoCommitMessage); - - ObjectId commitId = inserter.insert(commit); - inserter.flush(); - - RefUpdate ru = repo.updateRef(targetBranch); - 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, targetBranch), - ru.getRef(), rc); - default: - throw new JGitInternalException(MessageFormat.format( - JGitText.get().updatingRefFailed, - targetBranch, commitId.name(), rc)); - } - - return rw.parseCommit(commitId); - } - - private void addSubmodule(String name, String url, String path, - String revision, List copyfiles, List linkfiles, - Git git) throws GitAPIException, IOException { - assert (!repo.isBare()); - assert (git != null); - if (!linkfiles.isEmpty()) { - throw new UnsupportedOperationException( - JGitText.get().nonBareLinkFilesNotSupported); - } - - SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) - .setURI(url); - if (monitor != null) - add.setProgressMonitor(monitor); - - Repository subRepo = add.call(); - if (revision != null) { - try (Git sub = new Git(subRepo)) { - sub.checkout().setName(findRef(revision, subRepo)).call(); - } - subRepo.close(); - git.add().addFilepattern(path).call(); - } - for (CopyFile copyfile : copyfiles) { - copyfile.copy(); - git.add().addFilepattern(copyfile.dest).call(); - } + RegularSuperprojectWriter writer = new RegularSuperprojectWriter(repo, monitor); + return writer.write(filteredProjects); } /** @@ -910,13 +663,5 @@ static URI relativize(URI current, URI target) { return URI.create(j.toString()); } - private static String findRef(String ref, Repository repo) - throws IOException { - if (!ObjectId.isId(ref)) { - Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ - if (r != null) - return r.getName(); - } - return ref; - } + }