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.