This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 6b77c39d0f Revert file/Cherry-pick file commit the whole index. fixes
#8003 (#8007)
6b77c39d0f is described below
commit 6b77c39d0ff178da667997f0e6acaf4cba53c298
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Aug 20 10:39:31 2026 +0200
Revert file/Cherry-pick file commit the whole index. fixes #8003 (#8007)
---
.../java/org/apache/hop/git/GitPerspective.java | 116 +++++++++++++++++----
.../main/java/org/apache/hop/git/model/UIGit.java | 73 ++++++++++++-
.../hop/git/messages/messages_en_US.properties | 14 ++-
.../java/org/apache/hop/git/model/UIGitTest.java | 87 ++++++++++++++++
4 files changed, 261 insertions(+), 29 deletions(-)
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
index d182246341..3ae7005c9d 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
@@ -479,6 +479,11 @@ public class GitPerspective implements IHopPerspective {
fileMenuWidgets,
FILE_CONTEXT_MENU_SHOW_GRAPH_DIFF,
FileTypeUtils.isHopFileType(path));
+ // Reverting a file puts back the version of the commit's parent,
which a root commit
+ // does not have and a merge commit has more than one of
+ setMenuItemEnabled(
+ fileMenuWidgets, FILE_CONTEXT_MENU_REVERT,
commit.getParentCount() == 1);
+ setMenuItemEnabled(fileMenuWidgets, FILE_CONTEXT_MENU_CHERRY_PICK,
true);
} else {
event.doit = false;
}
@@ -854,28 +859,72 @@ public class GitPerspective implements IHopPerspective {
RevCommit commit = getSelectedCommit();
String path = getSelectedFile();
- if (path == null || commit == null) {
+ if (path == null || commit == null || commit.getParentCount() != 1) {
return;
}
- try {
- Git git = GitGuiPlugin.getInstance().getGit().getGit();
- String commitId = commit.getId().name();
+ UIGit git = GitGuiPlugin.getInstance().getGit();
+ String commitId = commit.getId().name();
+
+ // Undo what the commit did to the file: put back the version of its parent
+ //
+ String parentCommitId = git.getParentCommitId(commitId);
+ if (parentCommitId == null) {
+ return;
+ }
+
+ if (!confirmFileAction(
+ "GitPerspective.Dialog.RevertFile.Header",
+ "GitPerspective.Dialog.RevertFileConfirmation.Message",
+ path,
+ git.getShortenedName(commitId))) {
+ return;
+ }
- git.checkout().setStartPoint(commitId).addPath(path).call();
+ try {
+ git.restorePathFromCommit(path, parentCommitId);
- git.commit().setMessage("Revert " + path + " to version from " +
commitId).call();
+ commitFileAction(
+ git,
+ path,
+ BaseMessages.getString(
+ PKG,
+ "GitPerspective.RevertFile.CommitMessage",
+ path,
+ git.getShortenedName(commitId)));
refresh(true);
} catch (Exception e) {
new ErrorDialog(
getShell(),
- BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.RevertFile.Header"),
+ BaseMessages.getString(PKG,
"GitPerspective.Dialog.RevertFile.Header"),
BaseMessages.getString(PKG,
"GitGuiPlugin.Dialog.RevertFileError.Message"),
e);
}
}
+ /** Ask before a file action rewrites a file and commits it. */
+ private boolean confirmFileAction(String headerKey, String messageKey,
String path, String name) {
+ MessageBox dialog = new MessageBox(getShell(), SWT.ICON_QUESTION | SWT.YES
| SWT.NO);
+ dialog.setText(BaseMessages.getString(PKG, headerKey));
+ dialog.setMessage(BaseMessages.getString(PKG, messageKey, path, name));
+ return dialog.open() == SWT.YES;
+ }
+
+ /**
+ * Commit a single file, and say so when the file already held the content
it was going to be
+ * given: git has nothing to commit then.
+ */
+ private void commitFileAction(UIGit git, String path, String message) throws
HopException {
+ if (!git.commitPath(path, git.getAuthorName(VCS.WORKINGTREE), message)) {
+ MessageBox box = new MessageBox(getShell(), SWT.OK |
SWT.ICON_INFORMATION);
+ box.setText(BaseMessages.getString(PKG,
"GitPerspective.Dialog.FileUnchanged.Header"));
+ box.setMessage(
+ BaseMessages.getString(PKG,
"GitPerspective.Dialog.FileUnchanged.Message", path));
+ box.open();
+ }
+ }
+
@GuiMenuElement(
root = GUI_PLUGIN_HISTORY_CONTEXT_MENU_PARENT_ID,
parentId = GUI_PLUGIN_HISTORY_CONTEXT_MENU_PARENT_ID,
@@ -945,26 +994,42 @@ public class GitPerspective implements IHopPerspective {
RevCommit commit = getSelectedCommit();
String path = getSelectedFile();
- if (commit != null && path != null) {
- try {
- Git git = GitGuiPlugin.getInstance().getGit().getGit();
+ if (commit == null || path == null) {
+ return;
+ }
- String commitId = commit.getId().name();
+ UIGit git = GitGuiPlugin.getInstance().getGit();
+ String commitId = commit.getId().name();
- git.checkout().setStartPoint(commitId).addPath(path).call();
+ if (!confirmFileAction(
+ "GitPerspective.Dialog.CherryPickFile.Header",
+ "GitPerspective.Dialog.CherryPickFileConfirmation.Message",
+ path,
+ git.getShortenedName(commitId))) {
+ return;
+ }
- git.add().addFilepattern(path).call();
+ try {
+ // Take the file the way the commit left it
+ //
+ git.restorePathFromCommit(path, commitId);
- git.commit().setMessage("Cherry-pick " + path + " from commit " +
commitId).call();
+ commitFileAction(
+ git,
+ path,
+ BaseMessages.getString(
+ PKG,
+ "GitPerspective.CherryPickFile.CommitMessage",
+ path,
+ git.getShortenedName(commitId)));
- refresh(true);
- } catch (Exception e) {
- new ErrorDialog(
- getShell(),
- BaseMessages.getString(PKG,
"GitGuiPlugin.Dialog.CherryPickCommit.Header"),
- BaseMessages.getString(PKG,
"GitGuiPlugin.Dialog.CherryPickCommitError.Message"),
- e);
- }
+ refresh(true);
+ } catch (Exception e) {
+ new ErrorDialog(
+ getShell(),
+ BaseMessages.getString(PKG,
"GitPerspective.Dialog.CherryPickFile.Header"),
+ BaseMessages.getString(PKG,
"GitGuiPlugin.Dialog.CherryPickCommitError.Message"),
+ e);
}
}
@@ -1647,12 +1712,14 @@ public class GitPerspective implements IHopPerspective {
boolean isGitEnabled = git != null;
boolean isCommitSelected = false;
boolean isCommitInCurrentBranch = false;
+ boolean hasSingleParent = false;
if (isGitEnabled) {
RevCommit commit = getSelectedCommit();
if (commit != null) {
isCommitSelected = true;
isCommitInCurrentBranch = isCommitInCurrentBranch(commit);
+ hasSingleParent = commit.getParentCount() == 1;
}
}
@@ -1672,7 +1739,10 @@ public class GitPerspective implements IHopPerspective {
fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_SHOW_TEXT_DIFF,
isFileSelected);
fileToolBarWidgets.enableToolbarItem(
TOOLBAR_ITEM_FILE_SHOW_GRAPH_DIFF,
FileTypeUtils.isHopFileType(selectFile));
- fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_REVERT,
isFileSelected);
+ // Reverting a file puts back the version of the commit's parent, which a
root commit does not
+ // have and a merge commit has more than one of
+ fileToolBarWidgets.enableToolbarItem(
+ TOOLBAR_ITEM_FILE_REVERT, isFileSelected && hasSingleParent);
fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_CHERRY_PICK,
isFileSelected);
}
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
index 08a74c9a11..1bde3bf615 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
@@ -342,9 +342,7 @@ public class UIGit extends VCS {
* @throws HopException If an error occurs during the commit operation.
*/
public boolean commit(String authorName, String message, boolean amend)
throws HopException {
- PersonIdent author = RawParseUtils.parsePersonIdent(authorName);
- // Set the local time and use the system time zone
- PersonIdent committer = new PersonIdent(author, Instant.now());
+ PersonIdent committer = getCommitter(authorName);
try {
git.commit().setAuthor(committer).setMessage(message).setAmend(amend).call();
return true;
@@ -353,6 +351,11 @@ public class UIGit extends VCS {
}
}
+ /** The author of a commit, stamped with the local time and the system time
zone. */
+ private PersonIdent getCommitter(String authorName) {
+ return new PersonIdent(RawParseUtils.parsePersonIdent(authorName),
Instant.now());
+ }
+
/**
* The state git is in: whether it is in the middle of a merge, a
cherry-pick or a revert. A
* commit has to record the whole index while one of those is in progress,
so nothing may be left
@@ -419,6 +422,70 @@ public class UIGit extends VCS {
return commit(authorName, message, amend);
}
+ /**
+ * Make a single path look the way it does in a commit, in the working tree
and in the index. A
+ * path the commit does not have is removed here as well: not having it is
what that commit did to
+ * it.
+ *
+ * @param path the path to restore, relative to the repository root
+ * @param commitId the commit to take the path from
+ * @throws HopException when the path cannot be restored
+ */
+ public void restorePathFromCommit(String path, String commitId) throws
HopException {
+ String normalizedPath = normalizePathForJGit(path);
+ try {
+ if (existsInCommit(normalizedPath, commitId)) {
+ git.checkout().setStartPoint(commitId).addPath(normalizedPath).call();
+ } else {
+ // Not in that commit, so it should not be here either. A path which
is not in the index is
+ // simply left alone by git rm.
+ //
+ git.rm().addFilepattern(normalizedPath).call();
+ }
+ } catch (Exception e) {
+ throw new HopException("Error restoring '" + path + "' from commit '" +
commitId + "'", e);
+ }
+ }
+
+ /** Whether a commit has the given path in its tree. */
+ private boolean existsInCommit(String path, String commitId) throws
IOException {
+ RevCommit commit = resolve(commitId);
+ if (commit == null) {
+ return false;
+ }
+ try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), path,
commit.getTree())) {
+ return treeWalk != null;
+ }
+ }
+
+ /**
+ * Commit a single path, leaving anything else which is staged out of the
commit.
+ *
+ * @param path the only path to commit, relative to the repository root
+ * @param authorName the author of the commit, as "name <email>"
+ * @param message the commit message
+ * @return true when a commit was made, false when the path holds nothing to
commit
+ * @throws HopException when the commit fails
+ */
+ public boolean commitPath(String path, String authorName, String message)
throws HopException {
+ String normalizedPath = normalizePathForJGit(path);
+ try {
+ // Git has no empty commit to make here, and a path scoped commit of
nothing is an error
+ //
+ if
(!git.status().addPath(normalizedPath).call().hasUncommittedChanges()) {
+ return false;
+ }
+ git.commit()
+ .setOnly(normalizedPath)
+ .setAuthor(getCommitter(authorName))
+ .setMessage(message)
+ .call();
+ return true;
+ } catch (Exception e) {
+ throw new HopException("Error committing '" + path + "'", e);
+ }
+ }
+
public List<ObjectRevision> getRevisions() {
return getRevisions(null);
}
diff --git
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
index 9fb7f822ed..e7f71a1298 100644
---
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
+++
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
@@ -172,7 +172,15 @@ GitPerspective.Menu.MergeInto.Text=Merge ''{0}'' into
''{1}''
GitPerspective.Menu.CopyRevisionId.Text=Copy Revision Id
GitPerspective.Menu.CopyPath.Text=Copy Path
GitPerspective.Menu.CherryPickCommit.Text=Cherry-pick commit
-GitPerspective.Menu.CherryPickFile.Text=Cherry-pick file
+GitPerspective.Dialog.RevertFile.Header=Revert file
+GitPerspective.Dialog.RevertFileConfirmation.Message=Revert ''{0}'' to the
version before commit ''{1}''?
+GitPerspective.Dialog.CherryPickFile.Header=Cherry-pick file
+GitPerspective.Dialog.CherryPickFileConfirmation.Message=Cherry-pick ''{0}''
from commit ''{1}''?
+GitPerspective.Dialog.FileUnchanged.Header=Nothing to commit
+GitPerspective.Dialog.FileUnchanged.Message=''{0}'' already holds that version
+GitPerspective.RevertFile.CommitMessage=Revert {0} to the version before {1}
+GitPerspective.CherryPickFile.CommitMessage=Cherry-pick {0} from {1}
+GitPerspective.Menu.CherryPickFile.Text=Cherry-pick file...
GitPerspective.Menu.ResetToCommit.Text=Reset current branch to here...
GitPerspective.Menu.RevertCommit.Text=Revert commit...
GitPerspective.Menu.RevertFile.Text=Revert file...
@@ -185,10 +193,10 @@ GitPerspective.Ref.Tags.Label=Tags
GitPerspective.Search.Placeholder=Search commit...
GitPerspective.Toolbar.Refresh.Tooltip=Refresh
GitPerspective.Toolbar.CherryPickCommit.Tooltip=Cherry-pick commit
-GitPerspective.Toolbar.CherryPickFile.Tooltip=Cherry-pick file
+GitPerspective.Toolbar.CherryPickFile.Tooltip=Take this version of the file
and commit it
GitPerspective.Toolbar.ShowTextDiff.Tooltip=Show text diff...
GitPerspective.Toolbar.ShowGraphDiff.Tooltip=Show visual diff
-GitPerspective.Toolbar.RevertFile.Tooltip=Revert file...
+GitPerspective.Toolbar.RevertFile.Tooltip=Undo what this commit did to the
file and commit that
GitPerspective.Toolbar.ShowHiddenAllRef.Tooltip=Show or hidden all branches
and tags
GitPerspective.History.UncommittedChanges.Label={0} uncommitted change(s)
GitPerspective.History.Toolbar.CreateBranch.Tooltip=Create branch...
diff --git
a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
index 57cb71990e..ab9beeac27 100644
--- a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
+++ b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
@@ -1235,6 +1235,93 @@ public class UIGitTest extends RepositoryTestCase {
assertTrue(git.status().call().getAdded().contains("Staged.txt"));
}
+ /** Revert takes the version of the parent: what the file looked like before
that commit. */
+ @Test
+ public void testRestorePathFromCommitPutsBackTheContentOfThatCommit() throws
Exception {
+ writeTrashFile("Test.txt", "first");
+ git.add().addFilepattern("Test.txt").call();
+ RevCommit first = git.commit().setMessage("first").call();
+
+ writeTrashFile("Test.txt", "second");
+ git.add().addFilepattern("Test.txt").call();
+ git.commit().setMessage("second").call();
+
+ uiGit.restorePathFromCommit("Test.txt", first.getId().name());
+
+ assertEquals("first", read(new File(db.getWorkTree(), "Test.txt")));
+ assertTrue(git.status().call().getChanged().contains("Test.txt"));
+ }
+
+ /**
+ * Reverting a file which the commit added means removing it: its parent
does not have the file,
+ * so restoring that state deletes it here.
+ */
+ @Test
+ public void testRestorePathFromCommitRemovesAPathTheCommitDoesNotHave()
throws Exception {
+ RevCommit first = initialCommit();
+
+ writeTrashFile("Added.txt", "added later");
+ git.add().addFilepattern("Added.txt").call();
+ git.commit().setMessage("add a file").call();
+
+ uiGit.restorePathFromCommit("Added.txt", first.getId().name());
+
+ assertFalse(new File(db.getWorkTree(), "Added.txt").exists());
+ assertTrue(git.status().call().getRemoved().contains("Added.txt"));
+ }
+
+ /** Reverting a file which the commit deleted brings it back. */
+ @Test
+ public void testRestorePathFromCommitBringsBackADeletedPath() throws
Exception {
+ RevCommit first = initialCommit();
+
+ git.rm().addFilepattern("Test.txt").call();
+ git.commit().setMessage("delete the file").call();
+ assertFalse(new File(db.getWorkTree(), "Test.txt").exists());
+
+ uiGit.restorePathFromCommit("Test.txt", first.getId().name());
+
+ assertEquals("Hello world", read(new File(db.getWorkTree(), "Test.txt")));
+ }
+
+ /**
+ * The commit records the one path it was given. Anything else which
happened to be staged used to
+ * be swept into it under a message about a single file.
+ */
+ @Test
+ public void testCommitPathCommitsOnlyThatPath() throws Exception {
+ initialCommit();
+
+ writeTrashFile("Test.txt", "changed");
+ writeTrashFile("Unrelated.txt", "staged by someone else");
+ git.add().addFilepattern("Test.txt").call();
+ git.add().addFilepattern("Unrelated.txt").call();
+
+ assertTrue(uiGit.commitPath("Test.txt", "John Doe <[email protected]>",
"One file only"));
+
+ String head = uiGit.getCommitId(Constants.HEAD);
+ List<UIFile> committed =
uiGit.getStagedFiles(uiGit.getParentCommitId(head), head);
+ assertEquals(1, committed.size());
+ assertEquals("Test.txt", committed.get(0).getName());
+
+ // The unrelated file is still staged, waiting for a commit of its own
+ //
+ assertTrue(git.status().call().getAdded().contains("Unrelated.txt"));
+ }
+
+ /** A path which already holds the content asked for has nothing to commit.
*/
+ @Test
+ public void testCommitPathReportsWhenThereIsNothingToCommit() throws
Exception {
+ RevCommit first = initialCommit();
+
+ // Restoring the file to the version it already has changes nothing
+ //
+ uiGit.restorePathFromCommit("Test.txt", first.getId().name());
+
+ assertFalse(uiGit.commitPath("Test.txt", "John Doe <[email protected]>",
"Nothing to do"));
+ assertEquals(first,
git.getRepository().parseCommit(git.getRepository().resolve("HEAD")));
+ }
+
private void commitOnBranch(String branch, String file, String content)
throws Exception {
git.branchCreate().setName(branch).call();
git.checkout().setName(branch).call();