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>
This commit is contained in:
parent
6c8240a751
commit
e406d500de
|
@ -123,7 +123,7 @@ public void execute() throws BuildException {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
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;
|
||||||
|
@ -684,4 +685,19 @@ public void testCheckoutLink() throws Exception {
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -341,7 +341,8 @@ usage_fetchThinPack=fetch thin pack
|
||||||
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
|
||||||
|
|
|
@ -69,8 +69,11 @@ class Checkout extends TextBuiltin {
|
||||||
@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;
|
||||||
|
@ -103,7 +106,8 @@ protected void run() throws Exception {
|
||||||
} 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 {
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
|
|
||||||
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;
|
||||||
|
@ -109,7 +110,7 @@ public void setUp() throws Exception {
|
||||||
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");
|
||||||
|
@ -137,6 +138,18 @@ public void testCheckout() throws Exception {
|
||||||
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();
|
||||||
|
@ -165,7 +178,7 @@ public void testCheckoutWithConflict() throws Exception {
|
||||||
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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,9 @@ private Stage(int number) {
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
@ -269,7 +271,11 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException,
|
||||||
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();
|
||||||
|
@ -286,7 +292,7 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException,
|
||||||
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)
|
||||||
|
@ -666,10 +672,54 @@ public CheckoutCommand setOrphan(boolean orphan) {
|
||||||
* 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();
|
checkCallable();
|
||||||
this.force = force;
|
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();
|
||||||
|
this.forced = forced;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,8 @@ public CheckoutMetadata(EolStreamType eolStreamType,
|
||||||
|
|
||||||
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;
|
||||||
|
@ -427,11 +429,11 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
|
||||||
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
|
||||||
|
@ -821,14 +823,14 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
|
||||||
|
|
||||||
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;
|
||||||
|
@ -858,7 +860,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
|
||||||
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))
|
||||||
|
@ -878,7 +880,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
|
||||||
conflict(name, dce, h, m);
|
conflict(name, dce, h, m);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
keep(dce);
|
keep(dce, f);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -964,7 +966,7 @@ else if (m == null)
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -1027,7 +1029,7 @@ else if (m == null)
|
||||||
// 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
|
||||||
|
@ -1176,7 +1178,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode,
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1225,9 +1227,15 @@ private void conflict(String path, DirCacheEntry e, AbstractTreeIterator h, Abst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -1261,6 +1269,20 @@ public void setFailOnConflict(boolean failOnConflict) {
|
||||||
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…
Reference in New Issue