diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java index 27d322079..b75738c90 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java @@ -50,9 +50,11 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.junit.Test; @@ -213,6 +215,45 @@ public class RepoCommandTest extends RepositoryTestCase { assertEquals("The destination file has expected content", "world", content); } + @Test + public void testBareRepo() throws Exception { + Repository remoteDb = createBareRepository(); + Repository tempDb = createWorkRepository(); + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("\n") + .append("") + .append("") + .append("") + .append("") + .append(""); + JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", xmlContent.toString()); + RepoCommand command = new RepoCommand(remoteDb); + command.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml") + .setURI(rootUri) + .call(); + // Clone it + File directory = createTempDirectory("testBareRepo"); + CloneCommand clone = Git.cloneRepository(); + clone.setDirectory(directory); + clone.setURI(remoteDb.getDirectory().toURI().toString()); + Repository localDb = clone.call().getRepository(); + // The .gitmodules file should exist + File gitmodules = new File(localDb.getWorkTree(), ".gitmodules"); + assertTrue("The .gitmodules file exists", gitmodules.exists()); + // The first line of .gitmodules file should be expected + BufferedReader reader = new BufferedReader(new FileReader(gitmodules)); + String content = reader.readLine(); + reader.close(); + assertEquals("The first line of .gitmodules file is as expected.", + "[submodule \"foo\"]", content); + // The gitlink should be the same of remote head sha1 + String gitlink = localDb.resolve(Constants.HEAD + ":foo").name(); + String remote = defaultDb.resolve(Constants.HEAD).name(); + assertEquals("The gitlink is same as remote head", remote, gitlink); + } + private void resolveRelativeUris() { // Find the longest common prefix ends with "/" as rootUri. defaultUri = defaultDb.getDirectory().toURI().toString(); diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties index 29aa51ccd..33fdc2e15 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties @@ -1,5 +1,6 @@ copyFileFailed=Error occurred during execution of copyfile rule. errorNoDefault=Error: no default remote in file {0}. errorParsingManifestFile=Error occurred during parsing manifest file {0}. +errorRemoteUnavailable=Error remote {0} is unavailable. invalidManifest=Invalid manifest. repoCommitMessage=Added repo manifest. 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 475fbacaf..2766d7531 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -48,9 +48,11 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.channels.FileChannel; +import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -58,15 +60,32 @@ import java.util.Map; import java.util.Set; import org.eclipse.jgit.api.AddCommand; +import org.eclipse.jgit.api.LsRemoteCommand; 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.JGitInternalException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; 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.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.xml.sax.Attributes; import org.xml.sax.InputSource; @@ -89,10 +108,46 @@ public class RepoCommand extends GitCommand { private String path; private String uri; private String groups; + private PersonIdent author; + private RemoteReader callback; + private List bareProjects; private Git git; private ProgressMonitor monitor; + /** + * A callback to get head sha1 of a repository from its uri. + * + * We provided a default implementation {@link DefaultRemoteReader} to + * use ls-remote command to read the sha1 from the repository. Callers may + * have their own quicker implementation. + */ + public interface RemoteReader { + /** + * Read a remote repository's HEAD sha1. + * + * @param uri + * The URI of the remote repository + * @return the sha1 of the HEAD of the remote repository + */ + public ObjectId sha1(String uri) throws GitAPIException; + } + + /** A default implementation of {@link RemoteReader} callback. */ + public static class DefaultRemoteReader implements RemoteReader { + public ObjectId sha1(String uri) throws GitAPIException { + Collection refs = Git + .lsRemoteRepository() + .setRemote(uri) + .call(); + for (Ref ref : refs) { + if (Constants.HEAD.equals(ref.getName())) + return ref.getObjectId(); + } + return null; + } + } + private static class CopyFile { final String src; final String dest; @@ -293,6 +348,12 @@ public class RepoCommand extends GitCommand { } } + private static class RemoteUnavailableException extends GitAPIException { + RemoteUnavailableException(String uri, Throwable cause) { + super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri), cause); + } + } + /** * @param repo */ @@ -347,6 +408,32 @@ public class RepoCommand extends GitCommand { return this; } + /** + * Set the author/committer for the bare repository commit. + * + * For non-bare repositories, the current user will be used and this will be ignored. + * + * @param author + * @return this command + */ + public RepoCommand setAuthor(final PersonIdent author) { + this.author = author; + return this; + } + + /** + * Set the GetHeadFromUri callback. + * + * This is only used in bare repositories. + * + * @param callback + * @return this command + */ + public RepoCommand setRemoteReader(final RemoteReader callback) { + this.callback = callback; + return this; + } + @Override public RevCommit call() throws GitAPIException { checkCallable(); @@ -355,7 +442,15 @@ public class RepoCommand extends GitCommand { if (uri == null || uri.length() == 0) throw new IllegalArgumentException(JGitText.get().uriNotConfigured); - git = new Git(repo); + if (repo.isBare()) { + bareProjects = new ArrayList(); + if (author == null) + author = new PersonIdent(repo); + if (callback == null) + callback = new DefaultRemoteReader(); + } else + git = new Git(repo); + XmlManifest manifest = new XmlManifest(this, path, uri, groups); try { manifest.read(); @@ -363,23 +458,115 @@ public class RepoCommand extends GitCommand { throw new ManifestErrorException(e); } - return git - .commit() - .setMessage(RepoText.get().repoCommitMessage) - .call(); + if (repo.isBare()) { + DirCache index = DirCache.newInCore(); + DirCacheBuilder builder = index.builder(); + ObjectInserter inserter = repo.newObjectInserter(); + RevWalk rw = new RevWalk(repo); + + try { + Config cfg = new Config(); + for (Project proj : bareProjects) { + String name = proj.path; + String uri = proj.name; + cfg.setString("submodule", name, "path", name); //$NON-NLS-1$ //$NON-NLS-2$ + cfg.setString("submodule", name, "url", uri); //$NON-NLS-1$ //$NON-NLS-2$ + // create gitlink + final DirCacheEntry dcEntry = new DirCacheEntry(name); + ObjectId objectId; + try { + objectId = callback.sha1(uri); + } catch (GitAPIException e) { + // Something wrong getting the head sha1 + throw new RemoteUnavailableException(uri, e); + } catch (IllegalArgumentException e) { + // The revision from the manifest is malformed. + throw new ManifestErrorException(e); + } + if (objectId == null) + throw new RemoteUnavailableException(uri, null); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.GITLINK); + builder.add(dcEntry); + } + 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(Constants.HEAD + "^{commit}"); //$NON-NLS-1$ + 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(Constants.HEAD); + 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( + JGitText.get().couldNotLockHEAD, ru.getRef(), + rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, + Constants.HEAD, commitId.name(), rc)); + } + + return rw.parseCommit(commitId); + } catch (IOException e) { + throw new ManifestErrorException(e); + } finally { + rw.release(); + } + } else { + return git + .commit() + .setMessage(RepoText.get().repoCommitMessage) + .call(); + } } private void addSubmodule(String url, String name) throws SAXException { - SubmoduleAddCommand add = git - .submoduleAdd() - .setPath(name) - .setURI(url); - if (monitor != null) - add.setProgressMonitor(monitor); - try { - add.call(); - } catch (GitAPIException e) { - throw new SAXException(e); + if (repo.isBare()) { + Project proj = new Project(url, name, null); + bareProjects.add(proj); + } else { + SubmoduleAddCommand add = git + .submoduleAdd() + .setPath(name) + .setURI(url); + if (monitor != null) + add.setProgressMonitor(monitor); + try { + add.call(); + } catch (GitAPIException e) { + throw new SAXException(e); + } } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java index 1313fff0d..34db723b6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java @@ -62,6 +62,7 @@ public class RepoText extends TranslationBundle { /***/ public String copyFileFailed; /***/ public String errorNoDefault; /***/ public String errorParsingManifestFile; + /***/ public String errorRemoteUnavailable; /***/ public String invalidManifest; /***/ public String repoCommitMessage; }