]> source.dussan.org Git - jgit.git/commitdiff
Added merge support to CommitCommand 21/721/4
authorChristian Halstrick <christian.halstrick@sap.com>
Wed, 19 May 2010 08:01:25 +0000 (10:01 +0200)
committerChristian Halstrick <christian.halstrick@sap.com>
Thu, 20 May 2010 23:49:46 +0000 (01:49 +0200)
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 <christian.halstrick@sap.com>
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java
org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java

index d8fbb9578a90dd28ec88d630f8b2857c3b62e72b..a62045dc9fb7a49c9a5d080995d4408f1aa52246 100644 (file)
  */
 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);
+       }
 }
index 4cdf91416309ccdc6221417a8a2ef667c5919b11..e9ed28a198b983a8641be2e990396bda23178245 100644 (file)
@@ -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
index fbc17aad8e0ba68434f1342f1878e8f573a2b94e..a7c2e685c1e374fece30df6cebd1a02b540438c3 100644 (file)
@@ -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;
index 542c8214514d80b160358664072ddc982859167d..eef952e7cd14f5610626593065194612ba282819 100644 (file)
  */
 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<RevCommit> {
 
        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<ObjectId> parents = new LinkedList<ObjectId>();
+
        /**
         * @param repo
         */
@@ -96,6 +106,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
         *             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<RevCommit> {
         */
        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<RevCommit> {
                                                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<RevCommit> {
                                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<RevCommit> {
                                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<RevCommit> {
         * 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 (file)
index 0000000..833cf8d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> 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);
+       }
+}
index 37836f32468aecda773d96e35a69ec527c218b26..03ab629790ddda7b8f7aac051d96ff9a77eb216a 100644 (file)
@@ -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
        }
index e4d857bf8d21495654b2f0bebde341ee2e91ed72..233cecf310eaa4bdbe5ddebd46ed755b41dd62a8 100644 (file)
@@ -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<ObjectId> readMergeHeads() throws IOException {
+               File mergeHeadFile = new File(gitDir, Constants.MERGE_HEAD);
+               byte[] raw;
+               try {
+                       raw = IO.readFully(mergeHeadFile);
+               } catch (FileNotFoundException notFound) {
+                       return new LinkedList<ObjectId>();
+               }
+
+               if (raw.length == 0)
+                       throw new IOException("MERGE_HEAD file empty: " + mergeHeadFile);
+
+               LinkedList<ObjectId> heads = new LinkedList<ObjectId>();
+               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;
+       }
 }