From c3873b584f81bc5be3fdea53e9e0e76271a9a854 Mon Sep 17 00:00:00 2001 From: Christian Halstrick Date: Tue, 30 Jul 2013 14:00:18 +0200 Subject: [PATCH] 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 Also-by: Tobias Pfeifer Signed-off-by: Matthias Sohn --- .../eclipse/jgit/api/RebaseCommandTest.java | 147 +++++++-- .../eclipse/jgit/internal/JGitText.properties | 3 + .../org/eclipse/jgit/api/RebaseCommand.java | 307 +++--------------- .../org/eclipse/jgit/internal/JGitText.java | 3 + .../org/eclipse/jgit/lib/RebaseTodoFile.java | 187 +++++++++++ .../org/eclipse/jgit/lib/RebaseTodoLine.java | 269 +++++++++++++++ .../src/org/eclipse/jgit/lib/Repository.java | 36 ++ 7 files changed, 663 insertions(+), 289 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index ef5a1eaa0..c1d2b2213 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -60,10 +60,8 @@ import java.util.Iterator; 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.junit.RepositoryTestCase; 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.Before; 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 @@ public class RebaseCommandTest extends RepositoryTestCase { 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 class RebaseCommandTest extends RepositoryTestCase { + "# Comment line at end\n"; write(getTodoFile(), todo); - RebaseCommand rebaseCommand = git.rebase(); - List steps = rebaseCommand.loadSteps(); + List 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 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 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 class RebaseCommandTest extends RepositoryTestCase { + "reword 2222222 Commit 2\n"; write(getTodoFile(), todo); - RebaseCommand rebaseCommand = git.rebase(); - List steps = rebaseCommand.loadSteps(); + List 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 + 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 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 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 @@ -1672,8 +1770,8 @@ public class RebaseCommandTest extends RepositoryTestCase { RebaseResult res = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { - public void prepareSteps(List steps) { - steps.get(0).action = Action.REWORD; + public void prepareSteps(List steps) { + steps.get(0).setAction(Action.REWORD); } public String modifyCommitMessage(String commit) { return "rewritten commit message"; @@ -1713,8 +1811,8 @@ public class RebaseCommandTest extends RepositoryTestCase { RebaseResult res = git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { - public void prepareSteps(List steps) { - steps.get(0).action = Action.EDIT; + public void prepareSteps(List steps) { + steps.get(0).setAction(Action.EDIT); } public String modifyCommitMessage(String commit) { @@ -1741,8 +1839,7 @@ public class RebaseCommandTest extends RepositoryTestCase { } 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; } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 706dce7ce..5cf7af81d 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -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} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index a3f80c0f0..b8cbb9f04 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -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.DirCache; 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.ObjectId; 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 class RebaseCommand extends GitCommand { ObjectReader or = repo.newObjectReader(); - List steps = loadSteps(); + List 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 ids = or.resolve(step.commit); + if (Action.COMMENT.equals(step.getAction())) + continue; + Collection 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 class RebaseCommand extends GitCommand { 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 class RebaseCommand extends GitCommand { case EDIT: rebaseState.createFile(AMEND, commitToPick.name()); return stop(commitToPick); + case COMMENT: + break; } } finally { monitor.endTask(); @@ -541,67 +524,23 @@ public class RebaseCommand extends GitCommand { private void popSteps(int numSteps) throws IOException { if (numSteps == 0) return; - List todoLines = new ArrayList(); - List poppedLines = new ArrayList(); - 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 todoLines = new LinkedList(); + List poppedLines = new LinkedList(); + + 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 @@ public class RebaseCommand extends GitCommand { 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 toDoSteps = new ArrayList(); + 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 @@ public class RebaseCommand extends GitCommand { return true; } - List loadSteps() throws IOException { - byte[] buf = IO.readFully(rebaseState.getFile(GIT_REBASE_TODO)); - int ptr = 0; - int tokenBegin = 0; - ArrayList r = new ArrayList(); - 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 class RebaseCommand extends GitCommand { * @param steps * initial configuration of rebase interactive */ - void prepareSteps(List steps); + void prepareSteps(List steps); /** * Used for editing commit message on REWORD @@ -1077,107 +953,6 @@ public class RebaseCommand extends GitCommand { 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 class RebaseCommand extends GitCommand { 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)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 8ac971ab6..b4f99409b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -75,6 +75,7 @@ public class JGitText extends TranslationBundle { /***/ 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 class JGitText extends TranslationBundle { /***/ public String cachedPacksPreventsListingObjects; /***/ public String cannotBeCombined; /***/ public String cannotBeRecursiveWhenTreesAreIncluded; + /***/ public String cannotChangeActionOnComment; + /***/ public String cannotChangeToComment; /***/ public String cannotCombineSquashWithNoff; /***/ public String cannotCombineTreeFilterWithRevFilter; /***/ public String cannotCommitOnARepoWithState; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java new file mode 100644 index 000000000..a386fff0e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2013, Christian Halstrick + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.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 + * true if also comments should be reported + * @return the list of steps + * @throws IOException + */ + public List readRebaseTodo(String path, + boolean includeComments) throws IOException { + byte[] buf = IO.readFully(new File(repo.getDirectory(), path)); + int ptr = 0; + int tokenBegin = 0; + List r = new LinkedList(); + 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 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(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java new file mode 100644 index 000000000..747c4d3fc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2013, Christian Halstrick + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.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; + } + + /** + *

+ * Set a comment for this line that is used if this line's + * {@link RebaseTodoLine#action} is a {@link Action#COMMENT} + *

+ * It's allowed to unset the comment by calling + * setComment(null)
+ * A valid comment either starts with a hash (i.e. '#'), is an + * empty string, or consists of only spaces and tabs.
+ * If the argument newComment 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) + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 79f02c023..7df538ced 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -1563,4 +1563,40 @@ public abstract class Repository { } } + /** + * 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 + * true if also comments should be reported + * @return the list of steps + * @throws IOException + */ + public List 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 steps, + boolean append) + throws IOException { + new RebaseTodoFile(this).writeRebaseTodoFile(path, steps, append); + } }