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 &lt;email&gt;"
+   * @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();

Reply via email to