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 ad6314a466 Preserve uncommitted file changes during revert and 
cherry-pick (#9110)
ad6314a466 is described below

commit ad6314a4669add8467da2e8f6eca96948ac4468e
Author: Wu Shuwen <[email protected]>
AuthorDate: Wed Oct 28 21:12:48 2026 +0800

    Preserve uncommitted file changes during revert and cherry-pick (#9110)
    
    * Preserve uncommitted changes when restoring a file
    
    * Warn before replacing locally changed Git files
---
 .../java/org/apache/hop/git/GitPerspective.java    |  31 +++++++
 .../main/java/org/apache/hop/git/model/UIGit.java  |  17 ++++
 .../hop/git/messages/messages_en_US.properties     |   4 +
 .../java/org/apache/hop/git/model/UIGitTest.java   | 103 +++++++++++++++++++++
 4 files changed, 155 insertions(+)

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 726ec99be2..bc113928d9 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
@@ -869,6 +869,7 @@ public class GitPerspective implements IHopPerspective {
         dialog.setMessage(
             BaseMessages.getString(
                 PKG, 
"GitGuiPlugin.Dialog.RevertCommitRepositoryIsNotClean.Message"));
+        dialog.open();
         return;
       }
 
@@ -928,6 +929,9 @@ public class GitPerspective implements IHopPerspective {
     if (git == null) {
       return;
     }
+    if (showFileChangesWarning(git, path)) {
+      return;
+    }
     String commitId = commit.getId().name();
 
     // Undo what the commit did to the file: put back the version of its parent
@@ -967,6 +971,29 @@ public class GitPerspective implements IHopPerspective {
     }
   }
 
+  /** Show a precondition warning before a file action can overwrite local 
changes. */
+  private boolean showFileChangesWarning(UIGit git, String path) {
+    try {
+      if (!git.hasLocalChanges(path)) {
+        return false;
+      }
+    } catch (Exception e) {
+      new ErrorDialog(
+          getShell(),
+          BaseMessages.getString(PKG, 
"GitPerspective.Dialog.FileChangesCheckError.Header"),
+          BaseMessages.getString(PKG, 
"GitPerspective.Dialog.FileChangesCheckError.Message", path),
+          e);
+      return true;
+    }
+
+    MessageBox dialog = new MessageBox(getShell(), SWT.ICON_WARNING | SWT.OK);
+    dialog.setText(BaseMessages.getString(PKG, 
"GitPerspective.Dialog.FileChangesWarning.Header"));
+    dialog.setMessage(
+        BaseMessages.getString(PKG, 
"GitPerspective.Dialog.FileChangesWarning.Message", path));
+    dialog.open();
+    return true;
+  }
+
   /** 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);
@@ -1018,6 +1045,7 @@ public class GitPerspective implements IHopPerspective {
         dialog.setMessage(
             BaseMessages.getString(
                 PKG, 
"GitGuiPlugin.Dialog.CherryPickCommitRepositoryIsNotClean.Message"));
+        dialog.open();
         return;
       }
 
@@ -1076,6 +1104,9 @@ public class GitPerspective implements IHopPerspective {
     if (git == null) {
       return;
     }
+    if (showFileChangesWarning(git, path)) {
+      return;
+    }
     String commitId = commit.getId().name();
 
     if (!confirmFileAction(
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 dc1578ea44..b65ddf3c1d 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
@@ -426,6 +426,15 @@ public class UIGit extends VCS {
     return commit(authorName, message, amend);
   }
 
+  /** Whether the given path has staged, unstaged, or untracked local changes. 
*/
+  public boolean hasLocalChanges(String path) throws HopException {
+    try {
+      return 
!git.status().addPath(normalizePathForJGit(path)).call().isClean();
+    } catch (Exception e) {
+      throw new HopException("Error checking status for '" + path + "'", e);
+    }
+  }
+
   /**
    * 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
@@ -438,6 +447,12 @@ public class UIGit extends VCS {
   public void restorePathFromCommit(String path, String commitId) throws 
HopException {
     String normalizedPath = normalizePathForJGit(path);
     try {
+      if (hasLocalChanges(path)) {
+        throw new HopException(
+            "Cannot restore '"
+                + path
+                + "' because it has uncommitted changes. Commit or stash the 
file before continuing.");
+      }
       if (existsInCommit(normalizedPath, commitId)) {
         git.checkout().setStartPoint(commitId).addPath(normalizedPath).call();
       } else {
@@ -446,6 +461,8 @@ public class UIGit extends VCS {
         //
         git.rm().addFilepattern(normalizedPath).call();
       }
+    } catch (HopException e) {
+      throw e;
     } catch (Exception e) {
       throw new HopException("Error restoring '" + path + "' from commit '" + 
commitId + "'", e);
     }
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 e54a016bcd..0006f81014 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
@@ -176,6 +176,10 @@ 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.FileChangesWarning.Header=Uncommitted changes
+GitPerspective.Dialog.FileChangesWarning.Message=''{0}'' has uncommitted 
changes. Commit or stash them first.
+GitPerspective.Dialog.FileChangesCheckError.Header=Unable to check file status
+GitPerspective.Dialog.FileChangesCheckError.Message=Unable to check whether 
''{0}'' has uncommitted changes.
 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}
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 63d4670742..02b3be7fc8 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
@@ -1395,6 +1395,109 @@ public class UIGitTest extends RepositoryTestCase {
     assertTrue(git.status().call().getChanged().contains("Test.txt"));
   }
 
+  @Test
+  public void testRestorePathFromCommitPreservesUnstagedChanges() throws 
Exception {
+    RevCommit first = initialCommit();
+
+    writeTrashFile("Test.txt", "second");
+    git.add().addFilepattern("Test.txt").call();
+    git.commit().setMessage("second").call();
+
+    writeTrashFile("Test.txt", "MY LOCAL WORK");
+    String headBefore = git.getRepository().resolve(Constants.HEAD).name();
+
+    HopException error =
+        assertThrows(
+            HopException.class,
+            () -> uiGit.restorePathFromCommit("Test.txt", 
first.getId().name()));
+
+    assertTrue(error.getMessage().contains("uncommitted changes"));
+    assertEquals("MY LOCAL WORK", read(new File(db.getWorkTree(), 
"Test.txt")));
+    assertEquals(headBefore, 
git.getRepository().resolve(Constants.HEAD).name());
+    assertTrue(git.status().call().getModified().contains("Test.txt"));
+  }
+
+  @Test
+  public void testRestorePathFromCommitPreservesStagedChanges() throws 
Exception {
+    RevCommit first = initialCommit();
+
+    writeTrashFile("Test.txt", "second");
+    git.add().addFilepattern("Test.txt").call();
+    git.commit().setMessage("second").call();
+
+    writeTrashFile("Test.txt", "MY STAGED WORK");
+    git.add().addFilepattern("Test.txt").call();
+    String headBefore = git.getRepository().resolve(Constants.HEAD).name();
+
+    HopException error =
+        assertThrows(
+            HopException.class,
+            () -> uiGit.restorePathFromCommit("Test.txt", 
first.getId().name()));
+
+    assertTrue(error.getMessage().contains("uncommitted changes"));
+    assertEquals("MY STAGED WORK", read(new File(db.getWorkTree(), 
"Test.txt")));
+    assertEquals(headBefore, 
git.getRepository().resolve(Constants.HEAD).name());
+    assertTrue(git.status().call().getChanged().contains("Test.txt"));
+  }
+
+  @Test
+  public void testRestorePathFromCommitAllowsChangesToOtherPaths() throws 
Exception {
+    RevCommit first = initialCommit();
+
+    writeTrashFile("Test.txt", "second");
+    git.add().addFilepattern("Test.txt").call();
+    git.commit().setMessage("second").call();
+
+    writeTrashFile("Other.txt", "MY LOCAL WORK");
+    uiGit.restorePathFromCommit("Test.txt", first.getId().name());
+
+    assertEquals("Hello world", read(new File(db.getWorkTree(), "Test.txt")));
+    assertEquals("MY LOCAL WORK", read(new File(db.getWorkTree(), 
"Other.txt")));
+    assertTrue(git.status().call().getUntracked().contains("Other.txt"));
+  }
+
+  @Test
+  public void testRestorePathFromCommitPreservesChangesWhenRemovingAddedPath() 
throws Exception {
+    RevCommit first = initialCommit();
+
+    writeTrashFile("Added.txt", "added later");
+    git.add().addFilepattern("Added.txt").call();
+    git.commit().setMessage("add a file").call();
+
+    writeTrashFile("Added.txt", "MY LOCAL WORK");
+    String headBefore = git.getRepository().resolve(Constants.HEAD).name();
+    HopException error =
+        assertThrows(
+            HopException.class,
+            () -> uiGit.restorePathFromCommit("Added.txt", 
first.getId().name()));
+
+    assertTrue(error.getMessage().contains("uncommitted changes"));
+    assertEquals("MY LOCAL WORK", read(new File(db.getWorkTree(), 
"Added.txt")));
+    assertEquals(headBefore, 
git.getRepository().resolve(Constants.HEAD).name());
+    assertTrue(git.status().call().getModified().contains("Added.txt"));
+  }
+
+  @Test
+  public void 
testRestorePathFromCommitPreservesUntrackedFileWhenRestoringDeletedPath()
+      throws Exception {
+    RevCommit first = initialCommit();
+
+    git.rm().addFilepattern("Test.txt").call();
+    git.commit().setMessage("delete the file").call();
+    writeTrashFile("Test.txt", "MY LOCAL WORK");
+    String headBefore = git.getRepository().resolve(Constants.HEAD).name();
+
+    HopException error =
+        assertThrows(
+            HopException.class,
+            () -> uiGit.restorePathFromCommit("Test.txt", 
first.getId().name()));
+
+    assertTrue(error.getMessage().contains("uncommitted changes"));
+    assertEquals("MY LOCAL WORK", read(new File(db.getWorkTree(), 
"Test.txt")));
+    assertEquals(headBefore, 
git.getRepository().resolve(Constants.HEAD).name());
+    assertTrue(git.status().call().getUntracked().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.

Reply via email to