Browse Source

Do not allow non-ff-rebase if there are uncommitted changes

With this change jgit checks for uncommitted changes before a rebase is
started. This is also done by native git. One reason is that an abort
would override such changes. The check is skipped for a non-interactive
rebase when it will result in a fast-forward. In this case there can be
only checkout conflicts but no merge conflicts, so there cannot be an
abort which overrides uncommitted changes.

Bug: 422352
Change-Id: I1e0b59b2a4d80a686b67a6729e441924362b1236
Signed-off-by: Stefan Lay <stefan.lay@sap.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
tags/v3.2.0.201312181205-r
Stefan Lay 10 years ago
parent
commit
591998c2d6

+ 152
- 28
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java View File

import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.FileUtils;
// rebase // rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.CONFLICTS, result.getStatus());
assertEquals(1, result.getConflicts().size());
assertEquals("file2", result.getConflicts().get(0));
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
assertEquals(1, result.getUncommittedChanges().size());
assertEquals("file2", result.getUncommittedChanges().get(0));
} }


@Test @Test


RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.CONFLICTS, result.getStatus());
assertEquals(1, result.getConflicts().size());
assertEquals("file2", result.getConflicts().get(0));
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
assertEquals(1, result.getUncommittedChanges().size());
assertEquals("file2", result.getUncommittedChanges().get(0));


checkFile(uncommittedFile, "uncommitted file2"); checkFile(uncommittedFile, "uncommitted file2");
assertEquals(RepositoryState.SAFE, git.getRepository().getRepositoryState()); assertEquals(RepositoryState.SAFE, git.getRepository().getRepositoryState());
// rebase // rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.CONFLICTS, result.getStatus());
assertEquals(1, result.getConflicts().size());
assertEquals(FILE1, result.getConflicts().get(0));
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
assertEquals(1, result.getUncommittedChanges().size());
assertEquals(FILE1, result.getUncommittedChanges().get(0));
} }


@Test @Test
// rebase // rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.CONFLICTS, result.getStatus());
assertEquals(1, result.getConflicts().size());
assertEquals(FILE1, result.getConflicts().get(0));
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
assertEquals(1, result.getUncommittedChanges().size());
assertEquals(FILE1, result.getUncommittedChanges().get(0));
} }


@Test @Test
writeTrashFile("file0", "unstaged modified file0"); writeTrashFile("file0", "unstaged modified file0");


// rebase // rebase
assertEquals(Status.OK, git.rebase().setUpstream("refs/heads/master")
assertEquals(Status.UNCOMMITTED_CHANGES,
git.rebase().setUpstream("refs/heads/master")
.call().getStatus()); .call().getStatus());
} }


// rebase // rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.FAILED, result.getStatus());
// staged file0 causes DIRTY_INDEX
assertEquals(1, result.getFailingPaths().size());
assertEquals(MergeFailureReason.DIRTY_INDEX, result.getFailingPaths()
.get("file0"));
assertEquals("unstaged modified file0", read(file0));
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
assertEquals(1, result.getUncommittedChanges().size());
// index shall be unchanged // index shall be unchanged
assertEquals(indexState, indexState(CONTENT)); assertEquals(indexState, indexState(CONTENT));
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); assertEquals(RepositoryState.SAFE, db.getRepositoryState());
writeTrashFile("file0", "unstaged modified file0"); writeTrashFile("file0", "unstaged modified file0");


// rebase // rebase
assertEquals(Status.OK, git.rebase().setUpstream("refs/heads/master")
assertEquals(Status.UNCOMMITTED_CHANGES,
git.rebase().setUpstream("refs/heads/master")
.call().getStatus()); .call().getStatus());
} }


// rebase // rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master") RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call(); .call();
assertEquals(Status.FAILED, result.getStatus());
assertEquals(Status.UNCOMMITTED_CHANGES, result.getStatus());
// staged file0 causes DIRTY_INDEX // staged file0 causes DIRTY_INDEX
assertEquals(1, result.getFailingPaths().size());
assertEquals(MergeFailureReason.DIRTY_INDEX, result.getFailingPaths()
.get("file0"));
assertEquals(1, result.getUncommittedChanges().size());
assertEquals("unstaged modified file0", read(file0)); assertEquals("unstaged modified file0", read(file0));
// index shall be unchanged // index shall be unchanged
assertEquals(indexState, indexState(CONTENT)); assertEquals(indexState, indexState(CONTENT));
assertEquals(RepositoryState.SAFE, db.getRepositoryState()); assertEquals(RepositoryState.SAFE, db.getRepositoryState());
} }


@Test
public void testFastForwardRebaseWithModification() throws Exception {
// create file0 + file1, add and commit
writeTrashFile("file0", "file0");
writeTrashFile(FILE1, "file1");
git.add().addFilepattern("file0").addFilepattern(FILE1).call();
RevCommit commit = git.commit().setMessage("commit1").call();

// create topic branch
createBranch(commit, "refs/heads/topic");

// still on master / modify file1, add and commit
writeTrashFile(FILE1, "modified file1");
git.add().addFilepattern(FILE1).call();
git.commit().setMessage("commit2").call();

// checkout topic branch / modify file0 and add to index
checkoutBranch("refs/heads/topic");
writeTrashFile("file0", "modified file0 in index");
git.add().addFilepattern("file0").addFilepattern(FILE1).call();
// modify once more
writeTrashFile("file0", "modified file0");

// rebase
RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call();
assertEquals(Status.FAST_FORWARD, result.getStatus());
checkFile(new File(db.getWorkTree(), "file0"), "modified file0");
checkFile(new File(db.getWorkTree(), FILE1), "modified file1");
assertEquals("[file0, mode:100644, content:modified file0 in index]"
+ "[file1, mode:100644, content:modified file1]",
indexState(CONTENT));
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
}

@Test
public void testRebaseWithModificationShouldNotDeleteData()
throws Exception {
// create file0 + file1, add and commit
writeTrashFile("file0", "file0");
writeTrashFile(FILE1, "file1");
git.add().addFilepattern("file0").addFilepattern(FILE1).call();
RevCommit commit = git.commit().setMessage("commit1").call();

// create topic branch
createBranch(commit, "refs/heads/topic");

// still on master / modify file1, add and commit
writeTrashFile(FILE1, "modified file1");
git.add().addFilepattern(FILE1).call();
git.commit().setMessage("commit2").call();

// checkout topic branch / modify file1, add and commit
checkoutBranch("refs/heads/topic");
writeTrashFile(FILE1, "modified file1 on topic");
git.add().addFilepattern(FILE1).call();
git.commit().setMessage("commit3").call();

writeTrashFile("file0", "modified file0");

RebaseResult result = git.rebase().setUpstream("refs/heads/master")
.call();
// the following condition was true before commit 83b6ab233:
// jgit started the rebase and deleted the change on abort
// This test should verify that content was deleted
if (result.getStatus() == Status.STOPPED)
git.rebase().setOperation(Operation.ABORT).call();

checkFile(new File(db.getWorkTree(), "file0"), "modified file0");
checkFile(new File(db.getWorkTree(), FILE1),
"modified file1 on topic");
assertEquals("[file0, mode:100644, content:file0]"
+ "[file1, mode:100644, content:modified file1 on topic]",
indexState(CONTENT));
}

@Test @Test
public void testRebaseWithUncommittedDelete() throws Exception { public void testRebaseWithUncommittedDelete() throws Exception {
// create file0 + file1, add and commit // create file0 + file1, add and commit
// and attempt to rebase // and attempt to rebase
RebaseResult rebaseResult = git.rebase() RebaseResult rebaseResult = git.rebase()
.setUpstream("refs/heads/master").call(); .setUpstream("refs/heads/master").call();
assertEquals(Status.CONFLICTS, rebaseResult.getStatus());
assertEquals(1, rebaseResult.getConflicts().size());
assertEquals(FILE1, rebaseResult.getConflicts().get(0));
assertEquals(Status.UNCOMMITTED_CHANGES, rebaseResult.getStatus());
assertEquals(1, rebaseResult.getUncommittedChanges().size());
assertEquals(FILE1, rebaseResult.getUncommittedChanges().get(0));


checkFile(theFile, "dirty the file"); checkFile(theFile, "dirty the file");




} }


@Test
public void testInteractiveRebaseWithModificationShouldNotDeleteDataOnAbort()
throws Exception {
// create file0 + file1, add and commit
writeTrashFile("file0", "file0");
writeTrashFile(FILE1, "file1");
git.add().addFilepattern("file0").addFilepattern(FILE1).call();
git.commit().setMessage("commit1").call();

// modify file1, add and commit
writeTrashFile(FILE1, "modified file1");
git.add().addFilepattern(FILE1).call();
git.commit().setMessage("commit2").call();

// modify file1, add and commit
writeTrashFile(FILE1, "modified file1 a second time");
git.add().addFilepattern(FILE1).call();
git.commit().setMessage("commit3").call();

// modify file0, but do not commit
writeTrashFile("file0", "modified file0 in index");
git.add().addFilepattern("file0").addFilepattern(FILE1).call();
// do not commit
writeTrashFile("file0", "modified file0");

// start rebase
RebaseResult result = git.rebase().setUpstream("HEAD~2")
.runInteractively(new InteractiveHandler() {

public void prepareSteps(List<RebaseTodoLine> steps) {
steps.get(0).setAction(Action.EDIT);
steps.get(1).setAction(Action.PICK);
}

public String modifyCommitMessage(String commit) {
return commit;
}
}).call();
// the following condition was true before commit 83b6ab233:
// jgit started the rebase and deleted the change on abort
// This test should verify that content was deleted
if (result.getStatus() == Status.EDIT)
git.rebase().setOperation(Operation.ABORT).call();

checkFile(new File(db.getWorkTree(), "file0"), "modified file0");
checkFile(new File(db.getWorkTree(), "file1"),
"modified file1 a second time");
assertEquals("[file0, mode:100644, content:modified file0 in index]"
+ "[file1, mode:100644, content:modified file1 a second time]",
indexState(CONTENT));

}

private File getTodoFile() { private File getTodoFile() {
File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO); File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
return todoFile; return todoFile;

+ 12
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java View File

.resolve(upstreamCommitId)); .resolve(upstreamCommitId));
break; break;
case BEGIN: case BEGIN:
if (stopAfterInitialization
|| !walk.isMergedInto(
walk.parseCommit(repo.resolve(Constants.HEAD)),
upstreamCommit)) {
org.eclipse.jgit.api.Status status = Git.wrap(repo)
.status().call();
if (status.hasUncommittedChanges()) {
List<String> list = new ArrayList<String>();
list.addAll(status.getUncommittedChanges());
return RebaseResult.uncommittedChanges(list);
}
}
RebaseResult res = initFilesAndRewind(); RebaseResult res = initFilesAndRewind();
if (stopAfterInitialization) if (stopAfterInitialization)
return RebaseResult.INTERACTIVE_PREPARED_RESULT; return RebaseResult.INTERACTIVE_PREPARED_RESULT;

+ 39
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java View File

return false; return false;
} }
}, },
/**
* The repository contains uncommitted changes and the rebase is not a
* fast-forward
*
* @since 3.2
*/
UNCOMMITTED_CHANGES {
@Override
public boolean isSuccessful() {
return false;
}
},
/** /**
* Conflicts: checkout of target HEAD failed * Conflicts: checkout of target HEAD failed
*/ */


private List<String> conflicts; private List<String> conflicts;


private List<String> uncommittedChanges;

private RebaseResult(Status status) { private RebaseResult(Status status) {
this.status = status; this.status = status;
currentCommit = null; currentCommit = null;
return result; return result;
} }


/**
* Create <code>RebaseResult</code> with status
* {@link Status#UNCOMMITTED_CHANGES}
*
* @param uncommittedChanges
* the list of paths
* @return the RebaseResult
*/
static RebaseResult uncommittedChanges(List<String> uncommittedChanges) {
RebaseResult result = new RebaseResult(Status.UNCOMMITTED_CHANGES);
result.uncommittedChanges = uncommittedChanges;
return result;
}

/** /**
* @return the overall status * @return the overall status
*/ */
public List<String> getConflicts() { public List<String> getConflicts() {
return conflicts; return conflicts;
} }

/**
* @return the list of uncommitted changes if status is
* {@link Status#UNCOMMITTED_CHANGES}
*
* @since 3.2
*/
public List<String> getUncommittedChanges() {
return uncommittedChanges;
}

} }

+ 37
- 7
org.eclipse.jgit/src/org/eclipse/jgit/api/Status.java View File

package org.eclipse.jgit.api; package org.eclipse.jgit.api;


import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;




private final boolean clean; private final boolean clean;


private final boolean hasUncommittedChanges;;

/** /**
* @param diff * @param diff
*/ */
public Status(IndexDiff diff) { public Status(IndexDiff diff) {
super(); super();
this.diff = diff; this.diff = diff;
clean = diff.getAdded().isEmpty() //
&& diff.getChanged().isEmpty() //
&& diff.getRemoved().isEmpty() //
&& diff.getMissing().isEmpty() //
&& diff.getModified().isEmpty() //
&& diff.getUntracked().isEmpty() //
&& diff.getConflicting().isEmpty();
hasUncommittedChanges = !diff.getAdded().isEmpty() //
|| !diff.getChanged().isEmpty() //
|| !diff.getRemoved().isEmpty() //
|| !diff.getMissing().isEmpty() //
|| !diff.getModified().isEmpty() //
|| !diff.getConflicting().isEmpty();
clean = !hasUncommittedChanges //
&& diff.getUntracked().isEmpty();
} }


/** /**
return clean; return clean;
} }


/**
* @return true if any tracked file is changed
*
* @since 3.2
*/
public boolean hasUncommittedChanges() {
return hasUncommittedChanges;
}

/** /**
* @return list of files added to the index, not in HEAD (e.g. what you get * @return list of files added to the index, not in HEAD (e.g. what you get
* if you call 'git add ...' on a newly created file) * if you call 'git add ...' on a newly created file)
public Set<String> getIgnoredNotInIndex() { public Set<String> getIgnoredNotInIndex() {
return Collections.unmodifiableSet(diff.getIgnoredNotInIndex()); return Collections.unmodifiableSet(diff.getIgnoredNotInIndex());
} }

/**
* @return set of files and folders that are known to the repo and changed
* either in the index or in the working tree.
*
* @since 3.2
*/
public Set<String> getUncommittedChanges() {
Set<String> uncommittedChanges = new HashSet<String>();
uncommittedChanges.addAll(diff.getAdded());
uncommittedChanges.addAll(diff.getChanged());
uncommittedChanges.addAll(diff.getRemoved());
uncommittedChanges.addAll(diff.getMissing());
uncommittedChanges.addAll(diff.getModified());
uncommittedChanges.addAll(diff.getConflicting());
return uncommittedChanges;
}
} }

Loading…
Cancel
Save