From 6ca9843f3ebbea152969a8b795efce1d4ff15dbf Mon Sep 17 00:00:00 2001 From: Christian Halstrick Date: Wed, 19 May 2010 10:01:25 +0200 Subject: [PATCH] Added merge support to CommitCommand The CommitCommand should take care to create a merge commit if the file $GIT_DIR/MERGE_HEAD exists. It should then read the parents for the merge commit out of this file. It should also take care that when commiting a merge and no commit message was specified to read the message from $GIT_DIR/MERGE_MSG. Finally the CommitCommand should remove these files if the commit succeeded. Change-Id: I4e292115085099d5b86546d2021680cb1454266c Signed-off-by: Christian Halstrick --- .../jgit/api/CommitAndLogCommandTests.java | 47 ++++++++++-- .../org/eclipse/jgit/JGitText.properties | 2 + .../src/org/eclipse/jgit/JGitText.java | 2 + .../org/eclipse/jgit/api/CommitCommand.java | 72 ++++++++++++++++--- .../api/WrongRepositoryStateException.java | 55 ++++++++++++++ .../src/org/eclipse/jgit/lib/Constants.java | 6 ++ .../src/org/eclipse/jgit/lib/Repository.java | 56 ++++++++++++++- 7 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java index d8fbb9578..a62045dc9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java @@ -42,14 +42,22 @@ */ package org.eclipse.jgit.api; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RepositoryTestCase; import org.eclipse.jgit.revwalk.RevCommit; public class CommitAndLogCommandTests extends RepositoryTestCase { public void testSomeCommits() throws NoHeadException, NoMessageException, - UnmergedPathException, ConcurrentRefUpdateException { + UnmergedPathException, ConcurrentRefUpdateException, + JGitInternalException, WrongRepositoryStateException { // do 4 commits Git git = new Git(db); @@ -62,8 +70,8 @@ public class CommitAndLogCommandTests extends RepositoryTestCase { // check that all commits came in correctly PersonIdent defaultCommitter = new PersonIdent(db); - PersonIdent expectedAuthors[] = new PersonIdent[] { - defaultCommitter, committer, author, author }; + PersonIdent expectedAuthors[] = new PersonIdent[] { defaultCommitter, + committer, author, author }; PersonIdent expectedCommitters[] = new PersonIdent[] { defaultCommitter, committer, defaultCommitter, committer }; String expectedMessages[] = new String[] { "initial commit", @@ -82,7 +90,8 @@ public class CommitAndLogCommandTests extends RepositoryTestCase { // try to do a commit without specifying a message. Should fail! public void testWrongParams() throws UnmergedPathException, - NoHeadException, ConcurrentRefUpdateException { + NoHeadException, ConcurrentRefUpdateException, + JGitInternalException, WrongRepositoryStateException { Git git = new Git(db); try { git.commit().setAuthor(author).call(); @@ -95,7 +104,8 @@ public class CommitAndLogCommandTests extends RepositoryTestCase { // exceptions public void testMultipleInvocations() throws NoHeadException, ConcurrentRefUpdateException, NoMessageException, - UnmergedPathException { + UnmergedPathException, JGitInternalException, + WrongRepositoryStateException { Git git = new Git(db); CommitCommand commitCmd = git.commit(); commitCmd.setMessage("initial commit").call(); @@ -114,4 +124,31 @@ public class CommitAndLogCommandTests extends RepositoryTestCase { } catch (IllegalStateException e) { } } + + public void testMergeEmptyBranches() throws IOException, NoHeadException, + NoMessageException, ConcurrentRefUpdateException, + JGitInternalException, WrongRepositoryStateException { + Git git = new Git(db); + git.commit().setMessage("initial commit").call(); + RefUpdate r = db.updateRef("refs/heads/side"); + r.setNewObjectId(db.resolve(Constants.HEAD)); + assertEquals(r.forceUpdate(), RefUpdate.Result.NEW); + RevCommit second = git.commit().setMessage("second commit").setCommitter(committer).call(); + db.updateRef(Constants.HEAD).link("refs/heads/side"); + RevCommit firstSide = git.commit().setMessage("first side commit").setAuthor(author).call(); + + FileWriter wr = new FileWriter(new File(db.getDirectory(), + Constants.MERGE_HEAD)); + wr.write(ObjectId.toString(db.resolve("refs/heads/master"))); + wr.close(); + wr = new FileWriter(new File(db.getDirectory(), Constants.MERGE_MSG)); + wr.write("merging"); + wr.close(); + + RevCommit commit = git.commit().call(); + RevCommit[] parents = commit.getParents(); + assertEquals(parents[0], firstSide); + assertEquals(parents[1], second); + assertTrue(parents.length==2); + } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index 4cdf91416..e9ed28a19 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -27,6 +27,7 @@ blobNotFound=Blob not found: {0} blobNotFoundForPath=Blob not found: {0} for path: {1} cannotBeCombined=Cannot be combined. cannotCombineTreeFilterWithRevFilter=Cannot combine TreeFilter {0} with RefFilter {1}. +cannotCommitOnARepoWithState=Cannot commit on a repo with state: {0} cannotCommitWriteTo=Cannot commit write to {0} cannotConnectPipes=cannot connect pipes cannotConvertScriptToText=Cannot convert script to text @@ -137,6 +138,7 @@ errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on th errorReadingInfoRefs=error reading info/refs exceptionCaughtDuringExecutionOfCommitCommand=Exception caught during execution of commit command exceptionOccuredDuringAddingOfOptionToALogCommand=Exception occured during adding of {0} as option to a Log command +exceptionOccuredDuringReadingOfGIT_DIR=Exception occured during reading of $GIT_DIR/{0}. {1} expectedACKNAKFoundEOF=Expected ACK/NAK, found EOF expectedACKNAKGot=Expected ACK/NAK, got: {0} expectedBooleanStringValue=Expected boolean string value diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index fbc17aad8..a7c2e685c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -87,6 +87,7 @@ public class JGitText extends TranslationBundle { /***/ public String blobNotFoundForPath; /***/ public String cannotBeCombined; /***/ public String cannotCombineTreeFilterWithRevFilter; + /***/ public String cannotCommitOnARepoWithState; /***/ public String cannotCommitWriteTo; /***/ public String cannotConnectPipes; /***/ public String cannotConvertScriptToText; @@ -197,6 +198,7 @@ public class JGitText extends TranslationBundle { /***/ public String errorReadingInfoRefs; /***/ public String exceptionCaughtDuringExecutionOfCommitCommand; /***/ public String exceptionOccuredDuringAddingOfOptionToALogCommand; + /***/ public String exceptionOccuredDuringReadingOfGIT_DIR; /***/ public String expectedACKNAKFoundEOF; /***/ public String expectedACKNAKGot; /***/ public String expectedBooleanStringValue; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 542c82145..eef952e7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -42,8 +42,11 @@ */ package org.eclipse.jgit.api; +import java.io.File; import java.io.IOException; import java.text.MessageFormat; +import java.util.LinkedList; +import java.util.List; import org.eclipse.jgit.JGitText; import org.eclipse.jgit.dircache.DirCache; @@ -57,6 +60,7 @@ 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.lib.RepositoryState; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -76,6 +80,12 @@ public class CommitCommand extends GitCommand { private String message; + /** + * parents this commit should have. The current HEAD will be in this list + * and also all commits mentioned in .git/MERGE_HEAD + */ + private List parents = new LinkedList(); + /** * @param repo */ @@ -96,6 +106,8 @@ public class CommitCommand extends GitCommand { * when called without specifying a commit message * @throws UnmergedPathException * when the current index contained unmerged pathes (conflicts) + * @throws WrongRepositoryStateException + * when repository is not in the right state for committing * @throws JGitInternalException * a low-level exception of JGit has occurred. The original * exception can be retrieved by calling @@ -106,9 +118,14 @@ public class CommitCommand extends GitCommand { */ public RevCommit call() throws NoHeadException, NoMessageException, UnmergedPathException, ConcurrentRefUpdateException, - JGitInternalException { + JGitInternalException, WrongRepositoryStateException { checkCallable(); - processOptions(); + + RepositoryState state = repo.getRepositoryState(); + if (!state.canCommit()) + throw new WrongRepositoryStateException(MessageFormat.format( + JGitText.get().cannotCommitOnARepoWithState, state.name())); + processOptions(state); try { Ref head = repo.getRef(Constants.HEAD); @@ -117,7 +134,9 @@ public class CommitCommand extends GitCommand { JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); // determine the current HEAD and the commit it is referring to - ObjectId parentID = repo.resolve(Constants.HEAD + "^{commit}"); + ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); + if (headId != null) + parents.add(0, headId); // lock the index DirCache index = DirCache.lock(repo); @@ -134,8 +153,8 @@ public class CommitCommand extends GitCommand { commit.setCommitter(committer); commit.setAuthor(author); commit.setMessage(message); - if (parentID != null) - commit.setParentIds(new ObjectId[] { parentID }); + + commit.setParentIds(parents.toArray(new ObjectId[]{})); commit.setTreeId(indexTreeId); ObjectId commitId = repoWriter.writeCommit(commit); @@ -145,12 +164,20 @@ public class CommitCommand extends GitCommand { ru.setRefLogMessage("commit : " + revCommit.getShortMessage(), false); - ru.setExpectedOldObjectId(parentID); + ru.setExpectedOldObjectId(headId); Result rc = ru.update(); switch (rc) { case NEW: case FAST_FORWARD: setCallable(false); + if (state == RepositoryState.MERGING_RESOLVED) { + // Commit was successful. Now delete the files + // used for merge commits + new File(repo.getDirectory(), Constants.MERGE_HEAD) + .delete(); + new File(repo.getDirectory(), Constants.MERGE_MSG) + .delete(); + } return revCommit; case REJECTED: case LOCK_FAILURE: @@ -179,18 +206,41 @@ public class CommitCommand extends GitCommand { * Sets default values for not explicitly specified options. Then validates * that all required data has been provided. * + * @param state + * the state of the repository we are working on + * * @throws NoMessageException * if the commit message has not been specified */ - private void processOptions() throws NoMessageException { - if (message == null) - // as long as we don't suppport -C option we have to have - // an explicit message - throw new NoMessageException(JGitText.get().commitMessageNotSpecified); + private void processOptions(RepositoryState state) throws NoMessageException { if (committer == null) committer = new PersonIdent(repo); if (author == null) author = committer; + + // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files + if (state == RepositoryState.MERGING_RESOLVED) { + try { + parents = repo.readMergeHeads(); + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccuredDuringReadingOfGIT_DIR, + Constants.MERGE_HEAD, e)); + } + if (message == null) { + try { + message = repo.readMergeCommitMsg(); + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccuredDuringReadingOfGIT_DIR, + Constants.MERGE_MSG, e)); + } + } + } + if (message == null) + // as long as we don't suppport -C option we have to have + // an explicit message + throw new NoMessageException(JGitText.get().commitMessageNotSpecified); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java new file mode 100644 index 000000000..833cf8d6e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010, Christian Halstrick and + * other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v1.0 which accompanies this + * distribution, is reproduced below, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +/** + * Exception thrown when the state of the repository doesn't allow the execution + * of a certain command. E.g. when a CommitCommand should be executed on a + * repository with unresolved conflicts this exception will be thrown. + */ +public class WrongRepositoryStateException extends GitAPIException { + private static final long serialVersionUID = 1L; + + WrongRepositoryStateException(String message, Throwable cause) { + super(message, cause); + } + + WrongRepositoryStateException(String message) { + super(message); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 37836f324..03ab62979 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -518,6 +518,12 @@ public final class Constants { CHARSET = Charset.forName(CHARACTER_ENCODING); } + /** name of the file containing the commit msg for a merge commit */ + public static final String MERGE_MSG = "MERGE_MSG"; + + /** name of the file containing the IDs of the parents of a merge commit */ + public static final String MERGE_HEAD = "MERGE_HEAD"; + private Constants() { // Hide the default constructor } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index e4d857bf8..233cecf31 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -47,6 +47,7 @@ package org.eclipse.jgit.lib; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -61,12 +62,14 @@ import java.util.Set; import java.util.Vector; import java.util.concurrent.atomic.AtomicInteger; -import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.SystemReader; /** @@ -1338,4 +1341,55 @@ public class Repository { return new ReflogReader(this, ref.getName()); return null; } + + /** + * Return the information stored in the file $GIT_DIR/MERGE_MSG. In this + * file operations triggering a merge will store a template for the commit + * message of the merge commit. + * + * @return a String containing the content of the MERGE_MSG file or + * {@code null} if this file doesn't exist + * @throws IOException + */ + public String readMergeCommitMsg() throws IOException { + File mergeMsgFile = new File(gitDir, Constants.MERGE_MSG); + try { + return new String(IO.readFully(mergeMsgFile)); + } catch (FileNotFoundException e) { + // MERGE_MSG file has disappeared in the meantime + // ignore it + return null; + } + } + + /** + * Return the information stored in the file $GIT_DIR/MERGE_HEAD. In this + * file operations triggering a merge will store the IDs of all heads which + * should be merged together with HEAD. + * + * @return a list of {@link Commit}s which IDs are listed in the MERGE_HEAD + * file or {@code null} if this file doesn't exist. Also if the file + * exists but is empty {@code null} will be returned + * @throws IOException + */ + public List readMergeHeads() throws IOException { + File mergeHeadFile = new File(gitDir, Constants.MERGE_HEAD); + byte[] raw; + try { + raw = IO.readFully(mergeHeadFile); + } catch (FileNotFoundException notFound) { + return new LinkedList(); + } + + if (raw.length == 0) + throw new IOException("MERGE_HEAD file empty: " + mergeHeadFile); + + LinkedList heads = new LinkedList(); + for (int p = 0; p < raw.length;) { + heads.add(ObjectId.fromString(raw, p)); + p = RawParseUtils + .nextLF(raw, p + Constants.OBJECT_ID_STRING_LENGTH); + } + return heads; + } }