Enhance reading of git-rebase-todo formatted files

Reading and writing files formatted like the git-rebase-todo files was
hidden in the RebaseCommand. Certain constructs (like leading tabs and
spaces) have not been handled as in native git. Also the upcoming
rebase interactive feature in EGit needs reading/writing these files
independently from a RebaseCommand.

Therefore reading and writing those files has been moved to the
Repository class. RebaseCommand gets smaller because of that and doesn't
have to deal with reading/writing files.

Additional tests for empty todo-list files, or files containing comments
have been added.

Change-Id: I323f3619952fecdf28ddf50139a88e0bea34f5ba
Signed-off-by: Christian Halstrick <christian.halstrick@sap.com>
Also-by: Tobias Pfeifer <to.pfeifer@sap.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Christian Halstrick 2013-07-30 14:00:18 +02:00 committed by Matthias Sohn
parent e649287502
commit c3873b584f
7 changed files with 663 additions and 289 deletions

View File

@ -60,10 +60,8 @@
import java.util.List;
import org.eclipse.jgit.api.MergeResult.MergeStatus;
import org.eclipse.jgit.api.RebaseCommand.Action;
import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler;
import org.eclipse.jgit.api.RebaseCommand.Operation;
import org.eclipse.jgit.api.RebaseCommand.Step;
import org.eclipse.jgit.api.RebaseResult.Status;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.UnmergedPathsException;
@ -73,6 +71,8 @@
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RebaseTodoLine;
import org.eclipse.jgit.lib.RebaseTodoLine.Action;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.RepositoryState;
@ -85,6 +85,8 @@
import org.junit.Test;
public class RebaseCommandTest extends RepositoryTestCase {
private static final String GIT_REBASE_TODO = "rebase-merge/git-rebase-todo";
private static final String FILE1 = "file1";
protected Git git;
@ -1497,15 +1499,18 @@ private int countPicks() throws IOException {
try {
String line = br.readLine();
while (line != null) {
String actionToken = line.substring(0, line.indexOf(' '));
Action action = null;
try {
action = Action.parse(actionToken);
} catch (Exception e) {
// ignore
int firstBlank = line.indexOf(' ');
if (firstBlank != -1) {
String actionToken = line.substring(0, firstBlank);
Action action = null;
try {
action = Action.parse(actionToken);
} catch (Exception e) {
// ignore
}
if (Action.PICK.equals(action))
count++;
}
if (action != null)
count++;
line = br.readLine();
}
return count;
@ -1625,11 +1630,61 @@ public void testRebaseShouldBeAbleToHandleEmptyLinesInRebaseTodoFile()
+ "# Comment line at end\n";
write(getTodoFile(), todo);
RebaseCommand rebaseCommand = git.rebase();
List<Step> steps = rebaseCommand.loadSteps();
List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false);
assertEquals(2, steps.size());
assertEquals("1111111", steps.get(0).commit.name());
assertEquals("2222222", steps.get(1).commit.name());
assertEquals("1111111", steps.get(0).getCommit().name());
assertEquals("2222222", steps.get(1).getCommit().name());
}
@Test
public void testRebaseShouldNotFailIfUserAddCommentLinesInPrepareSteps()
throws Exception {
commitFile(FILE1, FILE1, "master");
RevCommit c2 = commitFile("file2", "file2", "master");
// update files on master
commitFile(FILE1, "blah", "master");
RevCommit c4 = commitFile("file2", "more change", "master");
RebaseResult res = git.rebase().setUpstream("HEAD~2")
.runInteractively(new InteractiveHandler() {
public void prepareSteps(List<RebaseTodoLine> steps) {
steps.add(0, new RebaseTodoLine(
"# Comment that should not be processed"));
}
public String modifyCommitMessage(String commit) {
fail("modifyCommitMessage() was not expected to be called");
return commit;
}
}).call();
assertEquals(RebaseResult.Status.FAST_FORWARD, res.getStatus());
RebaseResult res2 = git.rebase().setUpstream("HEAD~2")
.runInteractively(new InteractiveHandler() {
public void prepareSteps(List<RebaseTodoLine> steps) {
steps.get(0).setAction(Action.COMMENT); // delete
// RevCommit c4
}
public String modifyCommitMessage(String commit) {
fail("modifyCommitMessage() was not expected to be called");
return commit;
}
}).call();
assertEquals(RebaseResult.Status.OK, res2.getStatus());
ObjectId headId = db.resolve(Constants.HEAD);
RevWalk rw = new RevWalk(db);
RevCommit rc = rw.parseCommit(headId);
ObjectId head1Id = db.resolve(Constants.HEAD + "~1");
RevCommit rc1 = rw.parseCommit(head1Id);
assertEquals(rc.getFullMessage(), c4.getFullMessage());
assertEquals(rc1.getFullMessage(), c2.getFullMessage());
}
@Test
@ -1638,13 +1693,56 @@ public void testParseRewordCommand() throws Exception {
+ "reword 2222222 Commit 2\n";
write(getTodoFile(), todo);
RebaseCommand rebaseCommand = git.rebase();
List<Step> steps = rebaseCommand.loadSteps();
List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false);
assertEquals(2, steps.size());
assertEquals("1111111", steps.get(0).commit.name());
assertEquals("2222222", steps.get(1).commit.name());
assertEquals(Action.REWORD, steps.get(1).action);
assertEquals("1111111", steps.get(0).getCommit().name());
assertEquals("2222222", steps.get(1).getCommit().name());
assertEquals(Action.REWORD, steps.get(1).getAction());
}
@Test
public void testEmptyRebaseTodo() throws Exception {
write(getTodoFile(), "");
assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, true).size());
assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size());
}
@Test
public void testOnlyCommentRebaseTodo() throws Exception {
write(getTodoFile(), "# a b c d e\n# e f");
assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size());
List<RebaseTodoLine> lines = db.readRebaseTodo(GIT_REBASE_TODO, true);
assertEquals(2, lines.size());
for (RebaseTodoLine line : lines)
assertEquals(Action.COMMENT, line.getAction());
write(getTodoFile(), "# a b c d e\n# e f\n");
assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size());
lines = db.readRebaseTodo(GIT_REBASE_TODO, true);
assertEquals(2, lines.size());
for (RebaseTodoLine line : lines)
assertEquals(Action.COMMENT, line.getAction());
write(getTodoFile(), " \r\n# a b c d e\r\n# e f\r\n#");
assertEquals(0, db.readRebaseTodo(GIT_REBASE_TODO, false).size());
lines = db.readRebaseTodo(GIT_REBASE_TODO, true);
assertEquals(4, lines.size());
for (RebaseTodoLine line : lines)
assertEquals(Action.COMMENT, line.getAction());
}
@Test
public void testLeadingSpacesRebaseTodo() throws Exception {
String todo = " \t\t pick 1111111 Commit 1\n"
+ "\t\n"
+ "\treword 2222222 Commit 2\n";
write(getTodoFile(), todo);
List<RebaseTodoLine> steps = db.readRebaseTodo(GIT_REBASE_TODO, false);
assertEquals(2, steps.size());
assertEquals("1111111", steps.get(0).getCommit().name());
assertEquals("2222222", steps.get(1).getCommit().name());
assertEquals(Action.REWORD, steps.get(1).getAction());
}
@Test
@ -1672,8 +1770,8 @@ public void testRebaseInteractiveReword() throws Exception {
RebaseResult res = git.rebase().setUpstream("HEAD~2")
.runInteractively(new InteractiveHandler() {
public void prepareSteps(List<Step> steps) {
steps.get(0).action = Action.REWORD;
public void prepareSteps(List<RebaseTodoLine> steps) {
steps.get(0).setAction(Action.REWORD);
}
public String modifyCommitMessage(String commit) {
return "rewritten commit message";
@ -1713,8 +1811,8 @@ public void testRebaseInteractiveEdit() throws Exception {
RebaseResult res = git.rebase().setUpstream("HEAD~2")
.runInteractively(new InteractiveHandler() {
public void prepareSteps(List<Step> steps) {
steps.get(0).action = Action.EDIT;
public void prepareSteps(List<RebaseTodoLine> steps) {
steps.get(0).setAction(Action.EDIT);
}
public String modifyCommitMessage(String commit) {
@ -1741,8 +1839,7 @@ public String modifyCommitMessage(String commit) {
}
private File getTodoFile() {
File todoFile = new File(db.getDirectory(),
"rebase-merge/git-rebase-todo");
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
return todoFile;
}
}

View File

@ -13,6 +13,7 @@ anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
applyingCommit=Applying {0}
archiveFormatAlreadyAbsent=Archive format already absent: {0}
archiveFormatAlreadyRegistered=Archive format already registered: {0}
argumentIsNotAValidCommentString=Invalid comment: {0}
atLeastOnePathIsRequired=At least one path is required.
atLeastOnePatternIsRequired=At least one pattern is required.
atLeastTwoFiltersNeeded=At least two filters needed.
@ -38,6 +39,8 @@ cachedPacksPreventsIndexCreation=Using cached packs prevents index creation
cachedPacksPreventsListingObjects=Using cached packs prevents listing objects
cannotBeCombined=Cannot be combined.
cannotBeRecursiveWhenTreesAreIncluded=TreeWalk shouldn't be recursive when tree objects are included.
cannotChangeActionOnComment=Cannot change action on comment line in git-rebase-todo file, old action: {0}, new action: {1}.
cannotChangeToComment=Cannot change a non-comment line to a comment line.
cannotCombineSquashWithNoff=Cannot combine --squash with --no-ff.
cannotCombineTreeFilterWithRevFilter=Cannot combine TreeFilter {0} with RevFilter {1}.
cannotCommitOnARepoWithState=Cannot commit on a repo with state: {0}

View File

@ -42,21 +42,17 @@
*/
package org.eclipse.jgit.api;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -75,7 +71,6 @@
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
@ -83,6 +78,8 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.RebaseTodoLine;
import org.eclipse.jgit.lib.RebaseTodoLine.Action;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
@ -267,34 +264,18 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
ObjectReader or = repo.newObjectReader();
List<Step> steps = loadSteps();
List<RebaseTodoLine> steps = repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), false);
if (isInteractive()) {
interactiveHandler.prepareSteps(steps);
BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(
rebaseState.getFile(GIT_REBASE_TODO)),
Constants.CHARACTER_ENCODING));
fw.newLine();
try {
StringBuilder sb = new StringBuilder();
for (Step step : steps) {
sb.setLength(0);
sb.append(step.action.token);
sb.append(" "); //$NON-NLS-1$
sb.append(step.commit.name());
sb.append(" "); //$NON-NLS-1$
sb.append(RawParseUtils.decode(step.shortMessage)
.trim());
fw.write(sb.toString());
fw.newLine();
}
} finally {
fw.close();
}
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
steps, false);
}
for (Step step : steps) {
for (RebaseTodoLine step : steps) {
popSteps(1);
Collection<ObjectId> ids = or.resolve(step.commit);
if (Action.COMMENT.equals(step.getAction()))
continue;
Collection<ObjectId> ids = or.resolve(step.getCommit());
if (ids.size() != 1)
throw new JGitInternalException(
"Could not resolve uniquely the abbreviated object ID");
@ -334,7 +315,7 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
newHead = cherryPickResult.getNewHead();
}
}
switch (step.action) {
switch (step.getAction()) {
case PICK:
continue; // continue rebase process on pick command
case REWORD:
@ -347,6 +328,8 @@ public RebaseResult call() throws GitAPIException, NoHeadException,
case EDIT:
rebaseState.createFile(AMEND, commitToPick.name());
return stop(commitToPick);
case COMMENT:
break;
}
} finally {
monitor.endTask();
@ -541,67 +524,23 @@ String toAuthorScript(PersonIdent author) {
private void popSteps(int numSteps) throws IOException {
if (numSteps == 0)
return;
List<String> todoLines = new ArrayList<String>();
List<String> poppedLines = new ArrayList<String>();
File todoFile = rebaseState.getFile(GIT_REBASE_TODO);
File doneFile = rebaseState.getFile(DONE);
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream(todoFile), Constants.CHARACTER_ENCODING));
try {
// check if the line starts with a action tag (pick, skip...)
while (poppedLines.size() < numSteps) {
String popCandidate = br.readLine();
if (popCandidate == null)
break;
if (popCandidate.length() == 0)
continue;
if (popCandidate.charAt(0) == '#')
continue;
int spaceIndex = popCandidate.indexOf(' ');
boolean pop = false;
if (spaceIndex >= 0) {
String actionToken = popCandidate.substring(0, spaceIndex);
pop = Action.parse(actionToken) != null;
}
if (pop)
poppedLines.add(popCandidate);
else
todoLines.add(popCandidate);
}
String readLine = br.readLine();
while (readLine != null) {
todoLines.add(readLine);
readLine = br.readLine();
}
} finally {
br.close();
}
BufferedWriter todoWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(todoFile), Constants.CHARACTER_ENCODING));
try {
for (String writeLine : todoLines) {
todoWriter.write(writeLine);
todoWriter.newLine();
}
} finally {
todoWriter.close();
List<RebaseTodoLine> todoLines = new LinkedList<RebaseTodoLine>();
List<RebaseTodoLine> poppedLines = new LinkedList<RebaseTodoLine>();
for (RebaseTodoLine line : repo.readRebaseTodo(
rebaseState.getPath(GIT_REBASE_TODO), true)) {
if (poppedLines.size() >= numSteps
|| RebaseTodoLine.Action.COMMENT.equals(line.getAction()))
todoLines.add(line);
else
poppedLines.add(line);
}
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
todoLines, false);
if (poppedLines.size() > 0) {
// append here
BufferedWriter doneWriter = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(doneFile, true),
Constants.CHARACTER_ENCODING));
try {
for (String writeLine : poppedLines) {
doneWriter.write(writeLine);
doneWriter.newLine();
}
} finally {
doneWriter.close();
}
repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines,
true);
}
}
@ -666,28 +605,16 @@ else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) {
rebaseState.createFile(ONTO, upstreamCommit.name());
rebaseState.createFile(ONTO_NAME, upstreamCommitName);
rebaseState.createFile(INTERACTIVE, ""); //$NON-NLS-1$
BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(rebaseState.getFile(GIT_REBASE_TODO)),
Constants.CHARACTER_ENCODING));
fw.write("# Created by EGit: rebasing " + headId.name() + " onto "
+ upstreamCommit.name());
fw.newLine();
try {
StringBuilder sb = new StringBuilder();
ObjectReader reader = walk.getObjectReader();
for (RevCommit commit : cherryPickList) {
sb.setLength(0);
sb.append(Action.PICK.toToken());
sb.append(" "); //$NON-NLS-1$
sb.append(reader.abbreviate(commit).name());
sb.append(" "); //$NON-NLS-1$
sb.append(commit.getShortMessage());
fw.write(sb.toString());
fw.newLine();
}
} finally {
fw.close();
}
ArrayList<RebaseTodoLine> toDoSteps = new ArrayList<RebaseTodoLine>();
toDoSteps.add(new RebaseTodoLine("# Created by EGit: rebasing " + headId.name() //$NON-NLS-1$
+ " onto " + upstreamCommit.name())); //$NON-NLS-1$
ObjectReader reader = walk.getObjectReader();
for (RevCommit commit : cherryPickList)
toDoSteps.add(new RebaseTodoLine(Action.PICK, reader
.abbreviate(commit), commit.getShortMessage()));
repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO),
toDoSteps, false);
monitor.endTask();
@ -907,57 +834,6 @@ private boolean checkoutCommit(String headName, RevCommit commit)
return true;
}
List<Step> loadSteps() throws IOException {
byte[] buf = IO.readFully(rebaseState.getFile(GIT_REBASE_TODO));
int ptr = 0;
int tokenBegin = 0;
ArrayList<Step> r = new ArrayList<Step>();
while (ptr < buf.length) {
tokenBegin = ptr;
ptr = RawParseUtils.nextLF(buf, ptr);
int nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
int tokenCount = 0;
Step current = null;
while (tokenCount < 3 && nextSpace < ptr) {
switch (tokenCount) {
case 0:
String actionToken = new String(buf, tokenBegin, nextSpace
- tokenBegin - 1);
tokenBegin = nextSpace;
if (actionToken.charAt(0) == '#') {
tokenCount = 3;
break;
}
Action action = Action.parse(actionToken);
if (action != null)
current = new Step(Action.parse(actionToken));
break;
case 1:
if (current == null)
break;
nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
String commitToken = new String(buf, tokenBegin, nextSpace
- tokenBegin - 1);
tokenBegin = nextSpace;
current.commit = AbbreviatedObjectId
.fromString(commitToken);
break;
case 2:
if (current == null)
break;
nextSpace = ptr;
int length = ptr - tokenBegin;
current.shortMessage = new byte[length];
System.arraycopy(buf, tokenBegin, current.shortMessage, 0,
length);
r.add(current);
break;
}
tokenCount++;
}
}
return r;
}
/**
* @param upstream
@ -1066,7 +942,7 @@ public interface InteractiveHandler {
* @param steps
* initial configuration of rebase interactive
*/
void prepareSteps(List<Step> steps);
void prepareSteps(List<RebaseTodoLine> steps);
/**
* Used for editing commit message on REWORD
@ -1077,107 +953,6 @@ public interface InteractiveHandler {
String modifyCommitMessage(String commit);
}
/**
* Describes rebase actions
*/
public static enum Action {
/** Use commit */
PICK("pick", "p"), //$NON-NLS-1$ //$NON-NLS-2$
/** Use commit, but edit the commit message */
REWORD("reword", "r"), //$NON-NLS-1$ //$NON-NLS-2$
/** Use commit, but stop for amending */
EDIT("edit", "e"); // later add SQUASH, FIXUP, etc. //$NON-NLS-1$ //$NON-NLS-2$
private final String token;
private final String shortToken;
private Action(String token, String shortToken) {
this.token = token;
this.shortToken = shortToken;
}
/**
* @return full action token name
*/
public String toToken() {
return this.token;
}
@SuppressWarnings("nls")
@Override
public String toString() {
return "Action[" + token + "]";
}
static Action parse(String token) {
for (Action action : Action.values()) {
if (action.token.equals(token)
|| action.shortToken.equals(token))
return action;
}
throw new JGitInternalException(MessageFormat.format(
JGitText.get().unknownOrUnsupportedCommand, token,
Action.values()));
}
}
/**
* Describes single rebase step
*/
public static class Step {
Action action;
AbbreviatedObjectId commit;
byte[] shortMessage;
Step(Action action) {
this.action = action;
}
/**
* @return rebase action type
*/
public Action getAction() {
return action;
}
/**
* @param action
*/
public void setAction(Action action) {
this.action = action;
}
/**
* @return abbreviated commit SHA-1 of commit that action will be
* performed on
*/
public AbbreviatedObjectId getCommit() {
return commit;
}
/**
* @return short message commit of commit that action will be performed
* on
*/
public byte[] getShortMessage() {
return shortMessage;
}
@SuppressWarnings("nls")
@Override
public String toString() {
return "Step["
+ action
+ ", "
+ ((commit == null) ? "null" : commit)
+ ", "
+ ((shortMessage == null) ? "null" : new String(
shortMessage)) + "]";
}
}
PersonIdent parseAuthor(byte[] raw) {
if (raw.length == 0)
@ -1257,6 +1032,10 @@ public File getFile(String name) {
return new File(getDir(), name);
}
public String getPath(String name) {
return (getDir().getName() + "/" + name); //$NON-NLS-1$
}
private static String readFile(File directory, String fileName)
throws IOException {
byte[] content = IO.readFully(new File(directory, fileName));

View File

@ -75,6 +75,7 @@ public static JGitText get() {
/***/ public String applyingCommit;
/***/ public String archiveFormatAlreadyAbsent;
/***/ public String archiveFormatAlreadyRegistered;
/***/ public String argumentIsNotAValidCommentString;
/***/ public String atLeastOnePathIsRequired;
/***/ public String atLeastOnePatternIsRequired;
/***/ public String atLeastTwoFiltersNeeded;
@ -100,6 +101,8 @@ public static JGitText get() {
/***/ public String cachedPacksPreventsListingObjects;
/***/ public String cannotBeCombined;
/***/ public String cannotBeRecursiveWhenTreesAreIncluded;
/***/ public String cannotChangeActionOnComment;
/***/ public String cannotChangeToComment;
/***/ public String cannotCombineSquashWithNoff;
/***/ public String cannotCombineTreeFilterWithRevFilter;
/***/ public String cannotCommitOnARepoWithState;

View File

@ -0,0 +1,187 @@
/*
* Copyright (C) 2013, 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.lib;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
/**
* Offers methods to read and write files formatted like the git-rebase-todo
* file
*/
public class RebaseTodoFile {
private Repository repo;
/**
* @param repo
*/
public RebaseTodoFile(Repository repo) {
this.repo = repo;
}
/**
* Read a file formatted like the git-rebase-todo file. The "done" file is
* also formatted like the git-rebase-todo file. These files can be found in
* .git/rebase-merge/ or .git/rebase-append/ folders.
*
* @param path
* path to the file relative to the repository's git-dir. E.g.
* "rebase-merge/git-rebase-todo" or "rebase-append/done"
* @param includeComments
* <code>true</code> if also comments should be reported
* @return the list of steps
* @throws IOException
*/
public List<RebaseTodoLine> readRebaseTodo(String path,
boolean includeComments) throws IOException {
byte[] buf = IO.readFully(new File(repo.getDirectory(), path));
int ptr = 0;
int tokenBegin = 0;
List<RebaseTodoLine> r = new LinkedList<RebaseTodoLine>();
while (ptr < buf.length) {
RebaseTodoLine.Action action = null;
AbbreviatedObjectId commit = null;
tokenBegin = ptr;
ptr = RawParseUtils.nextLF(buf, ptr);
int lineStart = tokenBegin;
int lineEnd = ptr - 2;
if (lineEnd >= 0 && buf[lineEnd] == '\r')
lineEnd--;
// Handle comments
if (buf[tokenBegin] == '#') {
if (includeComments)
r.add(new RebaseTodoLine(RawParseUtils.decode(buf,
tokenBegin, lineEnd + 1)));
continue;
}
// skip leading spaces, tabs, CR
while (tokenBegin <= lineEnd
&& (buf[tokenBegin] == ' ' || buf[tokenBegin] == '\t' || buf[tokenBegin] == '\r'))
tokenBegin++;
// Handle empty lines (maybe empty after skipping leading
// whitespace)
if (tokenBegin > lineEnd) {
if (includeComments)
r.add(new RebaseTodoLine(RawParseUtils.decode(buf,
lineStart, 1 + lineEnd)));
continue;
}
int nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
int tokenCount = 0;
while (tokenCount < 3 && nextSpace < ptr) {
switch (tokenCount) {
case 0:
String actionToken = new String(buf, tokenBegin, nextSpace
- tokenBegin - 1);
tokenBegin = nextSpace;
action = RebaseTodoLine.Action.parse(actionToken);
break;
case 1:
if (action == null)
break;
nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
String commitToken = new String(buf, tokenBegin, nextSpace
- tokenBegin - 1);
tokenBegin = nextSpace;
commit = AbbreviatedObjectId.fromString(commitToken);
break;
case 2:
if (action == null)
break;
r.add(new RebaseTodoLine(action, commit, RawParseUtils
.decode(buf, tokenBegin, 1 + lineEnd)));
break;
}
tokenCount++;
}
}
return r;
}
/**
* Write a file formatted like a git-rebase-todo file.
*
* @param path
* path to the file relative to the repository's git-dir. E.g.
* "rebase-merge/git-rebase-todo" or "rebase-append/done"
* @param steps
* the steps to be written
* @param append
* whether to append to an existing file or to write a new file
* @throws IOException
*/
public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps,
boolean append) throws IOException {
BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(new File(repo.getDirectory(), path),
append), Constants.CHARACTER_ENCODING));
try {
StringBuilder sb = new StringBuilder();
for (RebaseTodoLine step : steps) {
sb.setLength(0);
if (RebaseTodoLine.Action.COMMENT.equals(step.action))
sb.append(step.getComment());
else {
sb.append(step.getAction().toToken());
sb.append(" "); //$NON-NLS-1$
sb.append(step.getCommit().name());
sb.append(" "); //$NON-NLS-1$
sb.append(step.getShortMessage().trim());
}
fw.write(sb.toString());
fw.newLine();
}
} finally {
fw.close();
}
}
}

View File

@ -0,0 +1,269 @@
/*
* Copyright (C) 2013, 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.lib;
import java.text.MessageFormat;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.internal.JGitText;
/**
* Describes a single line in a file formatted like the git-rebase-todo file.
*/
public class RebaseTodoLine {
/**
* Describes rebase actions
*/
@SuppressWarnings("nls")
public static enum Action {
/** Use commit */
PICK("pick", "p"),
/** Use commit, but edit the commit message */
REWORD("reword", "r"),
/** Use commit, but stop for amending */
EDIT("edit", "e"),
// TODO: add SQUASH, FIXUP, etc.
/**
* A comment in the file. Also blank lines (or lines containing only
* whitespaces) are reported as comments
*/
COMMENT("comment", "#");
private final String token;
private final String shortToken;
private Action(String token, String shortToken) {
this.token = token;
this.shortToken = shortToken;
}
/**
* @return full action token name
*/
public String toToken() {
return this.token;
}
@Override
public String toString() {
return "Action[" + token + "]";
}
/**
* @param token
* @return the Action
*/
static public Action parse(String token) {
for (Action action : Action.values()) {
if (action.token.equals(token)
|| action.shortToken.equals(token))
return action;
}
throw new JGitInternalException(MessageFormat.format(
JGitText.get().unknownOrUnsupportedCommand, token,
Action.values()));
}
}
Action action;
final AbbreviatedObjectId commit;
String shortMessage;
String comment;
/**
* Create a new comment line
*
* @param newComment
* the new comment
*/
public RebaseTodoLine(String newComment) {
this.action = Action.COMMENT;
setComment(newComment);
this.commit = null;
this.shortMessage = null;
}
/**
* Create a new non-comment line
*
* @param action
* @param commit
* @param shortMessage
*/
public RebaseTodoLine(Action action, AbbreviatedObjectId commit,
String shortMessage) {
this.action = action;
this.commit = commit;
this.shortMessage = shortMessage;
this.comment = null;
}
/**
* @return rebase action type
*/
public Action getAction() {
return action;
}
/**
* Set the action. It's not allowed to set a non-comment action on a line
* which was a comment line before. But you are allowed to set the comment
* action on a non-comment line and afterwards change the action back to
* non-comment.
*
* @param newAction
*/
public void setAction(Action newAction) {
if (!Action.COMMENT.equals(action) && Action.COMMENT.equals(newAction)) {
// transforming from non-comment to comment
if (comment == null)
// no comment was explicitly set. Take the old line as comment
// text
comment = "# " + action.token + " " //$NON-NLS-1$ //$NON-NLS-2$
+ ((commit == null) ? "null" : commit.name()) + " " //$NON-NLS-1$ //$NON-NLS-2$
+ ((shortMessage == null) ? "null" : shortMessage); //$NON-NLS-1$
} else if (Action.COMMENT.equals(action) && !Action.COMMENT.equals(newAction)) {
// transforming from comment to non-comment
if (commit == null)
throw new JGitInternalException(MessageFormat.format(
JGitText.get().cannotChangeActionOnComment, action,
newAction));
}
this.action = newAction;
}
/**
* <p>
* Set a comment for this line that is used if this line's
* {@link RebaseTodoLine#action} is a {@link Action#COMMENT}
* </p>
* It's allowed to unset the comment by calling
* <code>setComment(null)</code> <br>
* A valid comment either starts with a hash (i.e. <code>'#'</code>), is an
* empty string, or consists of only spaces and tabs.<br>
* If the argument <code>newComment</code> doesn't match these requirements
* an Exception is thrown.
*
* @param newComment
* the comment
*/
public void setComment(String newComment) {
if (newComment == null) {
this.comment = null;
return;
}
if (newComment.contains("\n") || newComment.contains("\r")) //$NON-NLS-1$ //$NON-NLS-2$
throw createInvalidCommentException(newComment);
if (newComment.trim().length() == 0 || newComment.startsWith("#")) { //$NON-NLS-1$
this.comment = newComment;
return;
}
throw createInvalidCommentException(newComment);
}
private static JGitInternalException createInvalidCommentException(
String newComment) {
IllegalArgumentException iae = new IllegalArgumentException(
MessageFormat.format(
JGitText.get().argumentIsNotAValidCommentString, newComment));
return new JGitInternalException(iae.getMessage(), iae);
}
/**
* @return abbreviated commit SHA-1 of commit that action will be performed
* on
*/
public AbbreviatedObjectId getCommit() {
return commit;
}
/**
* @return the first line of the commit message of the commit the action
* will be performed on.
*/
public String getShortMessage() {
return shortMessage;
}
/**
* @param shortMessage
*/
public void setShortMessage(String shortMessage) {
this.shortMessage = shortMessage;
}
/**
* @return a comment. If the line is a comment line then the comment is
* returned. Lines starting with # or blank lines or lines
* containing only spaces and tabs are considered as comment lines.
* The complete line is returned (e.g. including the '#')
*/
public String getComment() {
return comment;
}
@SuppressWarnings("nls")
@Override
public String toString() {
return "Step["
+ action
+ ", "
+ ((commit == null) ? "null" : commit)
+ ", "
+ ((shortMessage == null) ? "null" : shortMessage)
+ ", "
+ ((comment == null) ? "" : comment) + "]";
}
}

View File

@ -1563,4 +1563,40 @@ private void writeHeadsFile(List<ObjectId> heads, String filename)
}
}
/**
* Read a file formatted like the git-rebase-todo file. The "done" file is
* also formatted like the git-rebase-todo file. These files can be found in
* .git/rebase-merge/ or .git/rebase-append/ folders.
*
* @param path
* path to the file relative to the repository's git-dir. E.g.
* "rebase-merge/git-rebase-todo" or "rebase-append/done"
* @param includeComments
* <code>true</code> if also comments should be reported
* @return the list of steps
* @throws IOException
*/
public List<RebaseTodoLine> readRebaseTodo(String path,
boolean includeComments)
throws IOException {
return new RebaseTodoFile(this).readRebaseTodo(path, includeComments);
}
/**
* Write a file formatted like a git-rebase-todo file.
*
* @param path
* path to the file relative to the repository's git-dir. E.g.
* "rebase-merge/git-rebase-todo" or "rebase-append/done"
* @param steps
* the steps to be written
* @param append
* whether to append to an existing file or to write a new file
* @throws IOException
*/
public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps,
boolean append)
throws IOException {
new RebaseTodoFile(this).writeRebaseTodoFile(path, steps, append);
}
}