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>tags/v0.8.1
@@ -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); | |||
} | |||
} |
@@ -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 |
@@ -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; |
@@ -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<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); | |||
} | |||
/** |
@@ -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); | |||
} | |||
} |
@@ -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 | |||
} |
@@ -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; | |||
} | |||
} |