Browse Source

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 <christian.halstrick@sap.com>
stable-0.8
Christian Halstrick 15 years ago
parent
commit
6ca9843f3e
  1. 47
      org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java
  2. 2
      org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
  3. 2
      org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
  4. 72
      org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
  5. 55
      org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java
  6. 6
      org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
  7. 56
      org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java

47
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTests.java

@ -42,14 +42,22 @@
*/ */
package org.eclipse.jgit.api; 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.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.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RepositoryTestCase; import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
public class CommitAndLogCommandTests extends RepositoryTestCase { public class CommitAndLogCommandTests extends RepositoryTestCase {
public void testSomeCommits() throws NoHeadException, NoMessageException, public void testSomeCommits() throws NoHeadException, NoMessageException,
UnmergedPathException, ConcurrentRefUpdateException { UnmergedPathException, ConcurrentRefUpdateException,
JGitInternalException, WrongRepositoryStateException {
// do 4 commits // do 4 commits
Git git = new Git(db); Git git = new Git(db);
@ -62,8 +70,8 @@ public class CommitAndLogCommandTests extends RepositoryTestCase {
// check that all commits came in correctly // check that all commits came in correctly
PersonIdent defaultCommitter = new PersonIdent(db); PersonIdent defaultCommitter = new PersonIdent(db);
PersonIdent expectedAuthors[] = new PersonIdent[] { PersonIdent expectedAuthors[] = new PersonIdent[] { defaultCommitter,
defaultCommitter, committer, author, author }; committer, author, author };
PersonIdent expectedCommitters[] = new PersonIdent[] { PersonIdent expectedCommitters[] = new PersonIdent[] {
defaultCommitter, committer, defaultCommitter, committer }; defaultCommitter, committer, defaultCommitter, committer };
String expectedMessages[] = new String[] { "initial commit", 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! // try to do a commit without specifying a message. Should fail!
public void testWrongParams() throws UnmergedPathException, public void testWrongParams() throws UnmergedPathException,
NoHeadException, ConcurrentRefUpdateException { NoHeadException, ConcurrentRefUpdateException,
JGitInternalException, WrongRepositoryStateException {
Git git = new Git(db); Git git = new Git(db);
try { try {
git.commit().setAuthor(author).call(); git.commit().setAuthor(author).call();
@ -95,7 +104,8 @@ public class CommitAndLogCommandTests extends RepositoryTestCase {
// exceptions // exceptions
public void testMultipleInvocations() throws NoHeadException, public void testMultipleInvocations() throws NoHeadException,
ConcurrentRefUpdateException, NoMessageException, ConcurrentRefUpdateException, NoMessageException,
UnmergedPathException { UnmergedPathException, JGitInternalException,
WrongRepositoryStateException {
Git git = new Git(db); Git git = new Git(db);
CommitCommand commitCmd = git.commit(); CommitCommand commitCmd = git.commit();
commitCmd.setMessage("initial commit").call(); commitCmd.setMessage("initial commit").call();
@ -114,4 +124,31 @@ public class CommitAndLogCommandTests extends RepositoryTestCase {
} catch (IllegalStateException e) { } 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);
}
} }

2
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} blobNotFoundForPath=Blob not found: {0} for path: {1}
cannotBeCombined=Cannot be combined. cannotBeCombined=Cannot be combined.
cannotCombineTreeFilterWithRevFilter=Cannot combine TreeFilter {0} with RefFilter {1}. cannotCombineTreeFilterWithRevFilter=Cannot combine TreeFilter {0} with RefFilter {1}.
cannotCommitOnARepoWithState=Cannot commit on a repo with state: {0}
cannotCommitWriteTo=Cannot commit write to {0} cannotCommitWriteTo=Cannot commit write to {0}
cannotConnectPipes=cannot connect pipes cannotConnectPipes=cannot connect pipes
cannotConvertScriptToText=Cannot convert script to text cannotConvertScriptToText=Cannot convert script to text
@ -137,6 +138,7 @@ errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on th
errorReadingInfoRefs=error reading info/refs errorReadingInfoRefs=error reading info/refs
exceptionCaughtDuringExecutionOfCommitCommand=Exception caught during execution of commit command exceptionCaughtDuringExecutionOfCommitCommand=Exception caught during execution of commit command
exceptionOccuredDuringAddingOfOptionToALogCommand=Exception occured during adding of {0} as option to a Log 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 expectedACKNAKFoundEOF=Expected ACK/NAK, found EOF
expectedACKNAKGot=Expected ACK/NAK, got: {0} expectedACKNAKGot=Expected ACK/NAK, got: {0}
expectedBooleanStringValue=Expected boolean string value expectedBooleanStringValue=Expected boolean string value

2
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java

@ -87,6 +87,7 @@ public class JGitText extends TranslationBundle {
/***/ public String blobNotFoundForPath; /***/ public String blobNotFoundForPath;
/***/ public String cannotBeCombined; /***/ public String cannotBeCombined;
/***/ public String cannotCombineTreeFilterWithRevFilter; /***/ public String cannotCombineTreeFilterWithRevFilter;
/***/ public String cannotCommitOnARepoWithState;
/***/ public String cannotCommitWriteTo; /***/ public String cannotCommitWriteTo;
/***/ public String cannotConnectPipes; /***/ public String cannotConnectPipes;
/***/ public String cannotConvertScriptToText; /***/ public String cannotConvertScriptToText;
@ -197,6 +198,7 @@ public class JGitText extends TranslationBundle {
/***/ public String errorReadingInfoRefs; /***/ public String errorReadingInfoRefs;
/***/ public String exceptionCaughtDuringExecutionOfCommitCommand; /***/ public String exceptionCaughtDuringExecutionOfCommitCommand;
/***/ public String exceptionOccuredDuringAddingOfOptionToALogCommand; /***/ public String exceptionOccuredDuringAddingOfOptionToALogCommand;
/***/ public String exceptionOccuredDuringReadingOfGIT_DIR;
/***/ public String expectedACKNAKFoundEOF; /***/ public String expectedACKNAKFoundEOF;
/***/ public String expectedACKNAKGot; /***/ public String expectedACKNAKGot;
/***/ public String expectedBooleanStringValue; /***/ public String expectedBooleanStringValue;

72
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java

@ -42,8 +42,11 @@
*/ */
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jgit.JGitText; import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.dircache.DirCache; 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;
import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
@ -76,6 +80,12 @@ public class CommitCommand extends GitCommand<RevCommit> {
private String message; 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 * @param repo
*/ */
@ -96,6 +106,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
* when called without specifying a commit message * when called without specifying a commit message
* @throws UnmergedPathException * @throws UnmergedPathException
* when the current index contained unmerged pathes (conflicts) * when the current index contained unmerged pathes (conflicts)
* @throws WrongRepositoryStateException
* when repository is not in the right state for committing
* @throws JGitInternalException * @throws JGitInternalException
* a low-level exception of JGit has occurred. The original * a low-level exception of JGit has occurred. The original
* exception can be retrieved by calling * exception can be retrieved by calling
@ -106,9 +118,14 @@ public class CommitCommand extends GitCommand<RevCommit> {
*/ */
public RevCommit call() throws NoHeadException, NoMessageException, public RevCommit call() throws NoHeadException, NoMessageException,
UnmergedPathException, ConcurrentRefUpdateException, UnmergedPathException, ConcurrentRefUpdateException,
JGitInternalException { JGitInternalException, WrongRepositoryStateException {
checkCallable(); checkCallable();
processOptions();
RepositoryState state = repo.getRepositoryState();
if (!state.canCommit())
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().cannotCommitOnARepoWithState, state.name()));
processOptions(state);
try { try {
Ref head = repo.getRef(Constants.HEAD); Ref head = repo.getRef(Constants.HEAD);
@ -117,7 +134,9 @@ public class CommitCommand extends GitCommand<RevCommit> {
JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
// determine the current HEAD and the commit it is referring to // 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 // lock the index
DirCache index = DirCache.lock(repo); DirCache index = DirCache.lock(repo);
@ -134,8 +153,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
commit.setCommitter(committer); commit.setCommitter(committer);
commit.setAuthor(author); commit.setAuthor(author);
commit.setMessage(message); commit.setMessage(message);
if (parentID != null)
commit.setParentIds(new ObjectId[] { parentID }); commit.setParentIds(parents.toArray(new ObjectId[]{}));
commit.setTreeId(indexTreeId); commit.setTreeId(indexTreeId);
ObjectId commitId = repoWriter.writeCommit(commit); ObjectId commitId = repoWriter.writeCommit(commit);
@ -145,12 +164,20 @@ public class CommitCommand extends GitCommand<RevCommit> {
ru.setRefLogMessage("commit : " + revCommit.getShortMessage(), ru.setRefLogMessage("commit : " + revCommit.getShortMessage(),
false); false);
ru.setExpectedOldObjectId(parentID); ru.setExpectedOldObjectId(headId);
Result rc = ru.update(); Result rc = ru.update();
switch (rc) { switch (rc) {
case NEW: case NEW:
case FAST_FORWARD: case FAST_FORWARD:
setCallable(false); 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; return revCommit;
case REJECTED: case REJECTED:
case LOCK_FAILURE: case LOCK_FAILURE:
@ -179,18 +206,41 @@ public class CommitCommand extends GitCommand<RevCommit> {
* Sets default values for not explicitly specified options. Then validates * Sets default values for not explicitly specified options. Then validates
* that all required data has been provided. * that all required data has been provided.
* *
* @param state
* the state of the repository we are working on
*
* @throws NoMessageException * @throws NoMessageException
* if the commit message has not been specified * if the commit message has not been specified
*/ */
private void processOptions() throws NoMessageException { private void processOptions(RepositoryState state) 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);
if (committer == null) if (committer == null)
committer = new PersonIdent(repo); committer = new PersonIdent(repo);
if (author == null) if (author == null)
author = committer; 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);
} }
/** /**

55
org.eclipse.jgit/src/org/eclipse/jgit/api/WrongRepositoryStateException.java

@ -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);
}
}

6
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java

@ -518,6 +518,12 @@ public final class Constants {
CHARSET = Charset.forName(CHARACTER_ENCODING); 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() { private Constants() {
// Hide the default constructor // Hide the default constructor
} }

56
org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java

@ -47,6 +47,7 @@
package org.eclipse.jgit.lib; package org.eclipse.jgit.lib;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -61,12 +62,14 @@ import java.util.Set;
import java.util.Vector; import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.JGitText; import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.SystemReader;
/** /**
@ -1338,4 +1341,55 @@ public class Repository {
return new ReflogReader(this, ref.getName()); return new ReflogReader(this, ref.getName());
return null; 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;
}
} }

Loading…
Cancel
Save