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
} | } | ||||
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) { |
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)); | |||||
} | |||||
} | |||||
} | } |
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 |
@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 { |
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")); | ||||
} | } | ||||
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; | ||||
} | } | ||||
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 |