Browse Source

Fix "jgit checkout -f" to overwrite dirty worktree files

CheckoutCommand had a setForce() method. But this didn't correspond
to native git's 'git checkout -f' option. Deprecate the old setForce()
method and move its implementation to a new method setForceRefUpdate()
and use it to implement the -B option in the CLI class Checkout.

Add a setForced() method and use it to fix the associated '-f' option of
the CLI Checkout class to behave like native git's 'git checkout -f'
which overwrites dirty worktree files during checkout.

This is still not fully matching native git's behavior: updating
additionally dirty index entries is not done yet.

Bug: 530771
Change-Id: I776b78eb623b6ea0aca42f681788f2e4b1667f15
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
tags/v5.3.0.201901161700-m1
Christian Halstrick 6 years ago
parent
commit
e406d500de

+ 1
- 1
org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitCheckoutTask.java View File

} }


try { try {
checkout.setCreateBranch(createBranch).setForce(force)
checkout.setCreateBranch(createBranch).setForceRefUpdate(force)
.setName(branch); .setName(branch);
checkout.call(); checkout.call();
} catch (Exception e) { } catch (Exception e) {

+ 16
- 0
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java View File

import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
assertTrue(Files.isSymbolicLink(path)); assertTrue(Files.isSymbolicLink(path));
} }
} }

@Test
public void testCheckoutForce_Bug530771() throws Exception {
try (Git git = new Git(db)) {
File f = writeTrashFile("a", "Hello world");
git.add().addFilepattern("a").call();
git.commit().setMessage("create a").call();
writeTrashFile("a", "Goodbye world");
assertEquals("[]",
Arrays.toString(execute("git checkout -f HEAD")));
assertEquals("Hello world", read(f));
assertEquals("[a, mode:100644, content:Hello world]",
indexState(db, LocalDiskRepositoryTestCase.CONTENT));
}
}
} }

+ 2
- 1
org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties View File

usage_filesToAddContentFrom=Files to add content from usage_filesToAddContentFrom=Files to add content from
usage_fixAThinPackToBeComplete=fix a thin pack to be complete usage_fixAThinPackToBeComplete=fix a thin pack to be complete
usage_forEachRefOutput=for-each-ref output usage_forEachRefOutput=for-each-ref output
usage_forceCheckout=when switching branches, proceed even if the index or the working tree differs from HEAD
usage_forcedSwitchBranch=when switching branches do it forcefully. Succeed even if resetting an existing branch would cause commits to become unreachable
usage_forceCheckout=when checking out a commit succeed even if the working tree or the index is dirty. Overwrite the working tree or index in such cases
usage_forceClean=required to delete files or directories usage_forceClean=required to delete files or directories
usage_forceCreateBranchEvenExists=force create branch even exists usage_forceCreateBranchEvenExists=force create branch even exists
usage_forcedFetch=force ref update fetch option usage_forcedFetch=force ref update fetch option

+ 6
- 2
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java View File

@Option(name = "-b", usage = "usage_createBranchAndCheckout") @Option(name = "-b", usage = "usage_createBranchAndCheckout")
private boolean createBranch = false; private boolean createBranch = false;


@Option(name = "-B", usage = "usage_forcedSwitchBranch")
private boolean forceSwitchBranch = false;

@Option(name = "--force", aliases = { "-f" }, usage = "usage_forceCheckout") @Option(name = "--force", aliases = { "-f" }, usage = "usage_forceCheckout")
private boolean force = false;
private boolean forced = false;


@Option(name = "--orphan", usage = "usage_orphan") @Option(name = "--orphan", usage = "usage_orphan")
private boolean orphan = false; private boolean orphan = false;
} else { } else {
command.setCreateBranch(createBranch); command.setCreateBranch(createBranch);
command.setName(name); command.setName(name);
command.setForce(force);
command.setForceRefUpdate(forceSwitchBranch);
command.setForced(forced);
command.setOrphan(orphan); command.setOrphan(orphan);
} }
try { try {

+ 15
- 2
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java View File



import org.eclipse.jgit.api.CheckoutResult.Status; import org.eclipse.jgit.api.CheckoutResult.Status;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.InvalidRemoteException;
git.add().addFilepattern("Test.txt").call(); git.add().addFilepattern("Test.txt").call();
initialCommit = git.commit().setMessage("Initial commit").call(); initialCommit = git.commit().setMessage("Initial commit").call();


// create a master branch and switch to it
// create a test branch and switch to it
git.branchCreate().setName("test").call(); git.branchCreate().setName("test").call();
RefUpdate rup = db.updateRef(Constants.HEAD); RefUpdate rup = db.updateRef(Constants.HEAD);
rup.link("refs/heads/test"); rup.link("refs/heads/test");
assertEquals("refs/heads/master", git.getRepository().getFullBranch()); assertEquals("refs/heads/master", git.getRepository().getFullBranch());
} }


@Test
public void testCheckoutForced() throws Exception {
writeTrashFile("Test.txt", "Garbage");
try {
git.checkout().setName("master").call().getObjectId();
fail("Expected CheckoutConflictException didn't occur");
} catch (CheckoutConflictException e) {
}
assertEquals(initialCommit.getId(), git.checkout().setName("master")
.setForced(true).call().getObjectId());
}

@Test @Test
public void testCreateBranchOnCheckout() throws Exception { public void testCreateBranchOnCheckout() throws Exception {
git.checkout().setCreateBranch(true).setName("test2").call(); git.checkout().setCreateBranch(true).setName("test2").call();
assertEquals(Status.CONFLICTS, co.getResult().getStatus()); assertEquals(Status.CONFLICTS, co.getResult().getStatus());
assertTrue(co.getResult().getConflictList().contains("Test.txt")); assertTrue(co.getResult().getConflictList().contains("Test.txt"));
} }
git.checkout().setName("master").setForce(true).call();
git.checkout().setName("master").setForced(true).call();
assertThat(read("Test.txt"), is("Hello world")); assertThat(read("Test.txt"), is("Hello world"));
} }



+ 54
- 4
org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java View File



private String name; private String name;


private boolean force = false;
private boolean forceRefUpdate = false;

private boolean forced = false;


private boolean createBranch = false; private boolean createBranch = false;


try { try {
dco = new DirCacheCheckout(repo, headTree, dc, dco = new DirCacheCheckout(repo, headTree, dc,
newCommit.getTree()); newCommit.getTree());
dco.setFailOnConflict(!force);
dco.setFailOnConflict(true);
dco.setForce(forced);
if (forced) {
dco.setFailOnConflict(false);
}
dco.setProgressMonitor(monitor); dco.setProgressMonitor(monitor);
try { try {
dco.checkout(); dco.checkout();
ref = null; ref = null;
String toName = Repository.shortenRefName(name); String toName = Repository.shortenRefName(name);
RefUpdate refUpdate = repo.updateRef(Constants.HEAD, ref == null); RefUpdate refUpdate = repo.updateRef(Constants.HEAD, ref == null);
refUpdate.setForceUpdate(force);
refUpdate.setForceUpdate(forceRefUpdate);
refUpdate.setRefLogMessage(refLogMessage + " to " + toName, false); //$NON-NLS-1$ refUpdate.setRefLogMessage(refLogMessage + " to " + toName, false); //$NON-NLS-1$
Result updateResult; Result updateResult;
if (ref != null) if (ref != null)
* set to a new start-point; if false, the existing branch will * set to a new start-point; if false, the existing branch will
* not be changed * not be changed
* @return this instance * @return this instance
* @deprecated this method was badly named comparing its semantics to native
* git's checkout --force option, use
* {@link #setForceRefUpdate(boolean)} instead
*/ */
@Deprecated
public CheckoutCommand setForce(boolean force) { public CheckoutCommand setForce(boolean force) {
return setForceRefUpdate(force);
}

/**
* Specify to force the ref update in case of a branch switch.
*
* In releases prior to 5.2 this method was called setForce() but this name
* was misunderstood to implement native git's --force option, which is not
* true.
*
* @param forceRefUpdate
* if <code>true</code> and the branch with the given name
* already exists, the start-point of an existing branch will be
* set to a new start-point; if false, the existing branch will
* not be changed
* @return this instance
* @since 5.3
*/
public CheckoutCommand setForceRefUpdate(boolean forceRefUpdate) {
checkCallable();
this.forceRefUpdate = forceRefUpdate;
return this;
}

/**
* Allow a checkout even if the workingtree or index differs from HEAD. This
* matches native git's '--force' option.
*
* JGit releases before 5.2 had a method <code>setForce()</code> offering
* semantics different from this new <code>setForced()</code>. This old
* semantic can now be found in {@link #setForceRefUpdate(boolean)}
*
* @param forced
* if set to <code>true</code> then allow the checkout even if
* workingtree or index doesn't match HEAD. Overwrite workingtree
* files and index content with the new content in this case.
* @return this instance
* @since 5.3
*/
public CheckoutCommand setForced(boolean forced) {
checkCallable(); checkCallable();
this.force = force;
this.forced = forced;
return this; return this;
} }



+ 32
- 10
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java View File



private boolean failOnConflict = true; private boolean failOnConflict = true;


private boolean force = false;

private ArrayList<String> toBeDeleted = new ArrayList<>(); private ArrayList<String> toBeDeleted = new ArrayList<>();


private boolean initialCheckout; private boolean initialCheckout;
DirCacheEntry entry = i.getDirCacheEntry(); DirCacheEntry entry = i.getDirCacheEntry();
if (entry.getLastModified() == 0) if (entry.getLastModified() == 0)
entry.setLastModified(f.getEntryLastModified()); entry.setLastModified(f.getEntryLastModified());
keep(entry);
keep(entry, f);
} }
} else } else
// The index contains a folder // The index contains a folder
keep(i.getDirCacheEntry());
keep(i.getDirCacheEntry(), f);
} else { } else {
// There is no entry in the merge commit. Means: we want to delete // There is no entry in the merge commit. Means: we want to delete
// what's currently in the index and working tree // what's currently in the index and working tree


break; break;
case 0xDFD: // 3 4 case 0xDFD: // 3 4
keep(dce);
keep(dce, f);
break; break;
case 0xF0D: // 18 case 0xF0D: // 18
remove(name); remove(name);
break; break;
case 0xDFF: // 5 5b 6 6b case 0xDFF: // 5 5b 6 6b
if (equalIdAndMode(iId, iMode, mId, mMode)) if (equalIdAndMode(iId, iMode, mId, mMode))
keep(dce); // 5 6
keep(dce, f); // 5 6
else else
conflict(name, dce, h, m); // 5b 6b conflict(name, dce, h, m); // 5b 6b
break; break;
conflict(name, dce, h, m); // 9 conflict(name, dce, h, m); // 9
break; break;
case 0xFD0: // keep without a rule case 0xFD0: // keep without a rule
keep(dce);
keep(dce, f);
break; break;
case 0xFFD: // 12 13 14 case 0xFFD: // 12 13 14
if (equalIdAndMode(hId, hMode, iId, iMode)) if (equalIdAndMode(hId, hMode, iId, iMode))
conflict(name, dce, h, m); conflict(name, dce, h, m);
break; break;
default: default:
keep(dce);
keep(dce, f);
} }
return; return;
} }
if (initialCheckout) if (initialCheckout)
update(name, mId, mMode); update(name, mId, mMode);
else else
keep(dce);
keep(dce, f);
} else } else
conflict(name, dce, h, m); conflict(name, dce, h, m);
} }
// Nothing in Head // Nothing in Head
// Something in Index // Something in Index
// -> Merge contains nothing new. Keep the index. // -> Merge contains nothing new. Keep the index.
keep(dce);
keep(dce, f);
} else } else
// Merge contains something and it is not the same as Index // Merge contains something and it is not the same as Index
// Nothing in Head // Nothing in Head
// to the other one. // to the other one.
// -> In all three cases we don't touch index and file. // -> In all three cases we don't touch index and file.


keep(dce);
keep(dce, f);
} }
} }
} }
} }
} }


private void keep(DirCacheEntry e) {
private void keep(DirCacheEntry e, WorkingTreeIterator f)
throws IOException {
if (e != null && !FileMode.TREE.equals(e.getFileMode())) if (e != null && !FileMode.TREE.equals(e.getFileMode()))
builder.add(e); builder.add(e);
if (force) {
if (f.isModified(e, true, this.walk.getObjectReader())) {
checkoutEntry(repo, e, this.walk.getObjectReader());
}
}
} }


private void remove(String path) { private void remove(String path) {
this.failOnConflict = failOnConflict; this.failOnConflict = failOnConflict;
} }


/**
* If <code>true</code>, dirty worktree files may be overridden. If
* <code>false</code> dirty worktree files will not be overridden in order
* not to delete unsaved content. This corresponds to native git's 'git
* checkout -f' option. By default this option is set to false.
*
* @param force
* a boolean.
* @since 5.3
*/
public void setForce(boolean force) {
this.force = force;
}

/** /**
* This method implements how to handle conflicts when * This method implements how to handle conflicts when
* {@link #failOnConflict} is false * {@link #failOnConflict} is false

Loading…
Cancel
Save