You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/08/06 06:45:35 UTC
(hop) branch main updated: fix delete after git revert. fixes #7772 (#7775)
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 ec9a1171ba fix delete after git revert. fixes #7772 (#7775)
ec9a1171ba is described below
commit ec9a1171ba186747aad46f2c712df262c9c99c78
Author: Bart Maertens <ba...@know.bi>
AuthorDate: Thu Aug 6 08:45:29 2026 +0200
fix delete after git revert. fixes #7772 (#7775)
---
.../org/apache/hop/git/GitCommitPerspective.java | 9 +-
.../main/java/org/apache/hop/git/GitGuiPlugin.java | 91 +++++++++----------
.../main/java/org/apache/hop/git/model/UIGit.java | 67 +++++++++-----
.../hop/git/messages/messages_en_US.properties | 2 +-
.../java/org/apache/hop/git/model/UIGitTest.java | 100 +++++++++++++++++++++
5 files changed, 197 insertions(+), 72 deletions(-)
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
index 82c1f2ff2b..7d31689c11 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
@@ -586,14 +586,18 @@ public class GitCommitPerspective implements IHopPerspective {
List<String> filesToClose = new ArrayList<>();
List<String> filesToReload = new ArrayList<>();
+ List<String> addedFilesToDelete = new ArrayList<>();
for (UIFile file : files) {
// Absolute path for file explorer
String path = getAbsolutePath(git.getDirectory(), file.getName());
switch (file.getChangeType()) {
case ADD -> {
+ // Restoring an added file only unstages it, the file stays on disk unless the
+ // user explicitly asked to delete the local copies as well.
+ git.revertPath(file.getName());
if (deleteAddedFiles) {
- git.revertPath(file.getName());
+ addedFilesToDelete.add(file.getName());
filesToClose.add(path);
}
}
@@ -604,6 +608,9 @@ public class GitCommitPerspective implements IHopPerspective {
}
}
+ // The files which were added are untracked now: delete the local copies when asked
+ git.cleanPaths(addedFilesToDelete);
+
// Close tabs for restored files that were deleted (untracked/added)
ExplorerPerspective.getInstance().closeTabsForFilenames(filesToClose);
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
index d60a046b64..ba9e8480e6 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
@@ -27,7 +27,6 @@ import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -460,50 +459,31 @@ public class GitGuiPlugin
String selection = selectionDialog.open();
if (selection != null) {
int[] selectedNrs = selectionDialog.getSelectionIndeces();
-
- // Only close tabs for files that will be deleted (untracked/added); reload the rest
- Set<String> pathsThatWillBeDeleted =
- git.getRevertPathFilesThatWillBeDeleted(relativePath);
- List<String> filenamesToClose = new ArrayList<>();
- List<String> filenamesToReload = new ArrayList<>();
+ List<String> selectedPaths = new ArrayList<>();
for (int selectedNr : selectedNrs) {
- String filePath = files[selectedNr];
- String fullFilename;
- try {
- FileObject fileObj =
- HopVfs.getFileObject(new File(git.getDirectory(), filePath).getAbsolutePath());
- fullFilename =
- fileObj.exists()
- ? HopVfs.getFilename(fileObj)
- : new File(git.getDirectory(), filePath).getAbsolutePath();
- } catch (Exception ignored) {
- fullFilename = new File(git.getDirectory(), filePath).getAbsolutePath();
- }
- if (pathsThatWillBeDeleted.contains(filePath)) {
- filenamesToClose.add(fullFilename);
- } else {
- filenamesToReload.add(fullFilename);
- }
+ selectedPaths.add(files[selectedNr]);
}
- for (int selectedNr : selectedNrs) {
- String file = files[selectedNr];
- git.revertPath(file);
- }
+ // New files (untracked or added) are only unstaged by the revert, they stay on disk.
+ // Removing those takes an explicit git clean, see below.
+ //
+ Set<String> newPaths = git.getNewRevertPathFiles(relativePath);
- // Close tabs for reverted files that were deleted (untracked/added)
- ExplorerPerspective.getInstance().closeTabsForFilenames(filenamesToClose);
- // Reload tabs for reverted files that still exist (changed/missing/uncommitted)
- ExplorerPerspective.getInstance().reloadTabsForFilenames(filenamesToReload);
+ for (String filePath : selectedPaths) {
+ git.revertPath(filePath);
+ }
- // When a folder was selected, ask if user wants to run git clean (yes/no)
+ // When a folder was selected and new files were unstaged, ask if we should also
+ // remove those files with git clean (yes/no)
+ //
boolean isFolder = false;
try {
isFolder = HopVfs.getFileObject(explorerFile.getFilename()).isFolder();
} catch (Exception ignored) {
// not a folder
}
- if (isFolder) {
+ List<String> pathsToClean = selectedPaths.stream().filter(newPaths::contains).toList();
+ if (isFolder && !pathsToClean.isEmpty()) {
MessageBox cleanBox =
new MessageBox(
HopGui.getInstance().getShell(), SWT.YES | SWT.NO | SWT.ICON_QUESTION);
@@ -511,25 +491,38 @@ public class GitGuiPlugin
BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanConfirm.Header"));
cleanBox.setMessage(
BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanConfirm.Message"));
- if ((cleanBox.open() & SWT.YES) != 0) {
- Set<String> foldersToClean = new HashSet<>();
- foldersToClean.add(relativePath);
- for (int selectedNr : selectedNrs) {
- String filePath = files[selectedNr];
- int lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
- String parentFolder = (lastSlash <= 0) ? "" : filePath.substring(0, lastSlash);
- foldersToClean.add(parentFolder);
- }
- for (String folder : foldersToClean) {
- try {
- git.cleanPath(folder);
- } catch (Exception cleanEx) {
- LogChannel.UI.logError("Git clean failed for " + folder, cleanEx);
- }
+ if (cleanBox.open() == SWT.YES) {
+ try {
+ git.cleanPaths(pathsToClean);
+ } catch (Exception cleanEx) {
+ LogChannel.UI.logError("Git clean failed for " + relativePath, cleanEx);
}
}
}
+ // Close the tabs of the files which are no longer on disk, reload the others
+ //
+ List<String> filenamesToClose = new ArrayList<>();
+ List<String> filenamesToReload = new ArrayList<>();
+ for (String filePath : selectedPaths) {
+ File file = new File(git.getDirectory(), filePath);
+ String fullFilename;
+ try {
+ FileObject fileObj = HopVfs.getFileObject(file.getAbsolutePath());
+ fullFilename =
+ fileObj.exists() ? HopVfs.getFilename(fileObj) : file.getAbsolutePath();
+ } catch (Exception ignored) {
+ fullFilename = file.getAbsolutePath();
+ }
+ if (file.exists()) {
+ filenamesToReload.add(fullFilename);
+ } else {
+ filenamesToClose.add(fullFilename);
+ }
+ }
+ ExplorerPerspective.getInstance().closeTabsForFilenames(filenamesToClose);
+ ExplorerPerspective.getInstance().reloadTabsForFilenames(filenamesToReload);
+
// Show confirmation message once after all files have been reverted
MessageBox box =
new MessageBox(HopGui.getInstance().getShell(), SWT.OK | SWT.ICON_INFORMATION);
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 8944fcb759..e97965ebab 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
@@ -51,7 +51,6 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.hopgui.HopGui;
-import org.eclipse.jgit.api.CleanCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.DiffCommand;
import org.eclipse.jgit.api.Git;
@@ -902,9 +901,16 @@ public class UIGit extends VCS {
String normalizedPath = normalizePathForJGit(path);
// Revert files to HEAD state
Status status = git.status().addPath(normalizedPath).call();
- if (!status.getUntracked().isEmpty() || !status.getAdded().isEmpty()) {
- resetPath(normalizedPath);
- org.apache.commons.io.FileUtils.deleteQuietly(new File(directory, normalizedPath));
+ boolean isAdded = status.getAdded().contains(normalizedPath);
+ if (isAdded || status.getUntracked().contains(normalizedPath)) {
+ // The file is new: it doesn't exist in HEAD. Simply unstage it (if it was staged) and
+ // leave it on disk. Like git itself, revert never removes working tree files: removing
+ // untracked files takes an explicit git clean.
+ //
+ if (isAdded) {
+ resetPath(normalizedPath);
+ }
+ return;
}
/*
@@ -922,24 +928,43 @@ public class UIGit extends VCS {
}
/**
- * Clean untracked files and directories under the given path (e.g. a folder).
+ * Remove the given untracked files from the working tree: a targeted <code>git clean</code>.
+ * Paths which git doesn't report as untracked (tracked files, ignored files) are skipped, so this
+ * can never remove content which is under version control.
*
- * @param path The path to clean (relative to repo root)
+ * @param paths The paths to clean (relative to the repository root)
* @throws HopException when the clean operation fails
*/
- public void cleanPath(String path) throws HopException {
+ public void cleanPaths(Collection<String> paths) throws HopException {
+ if (paths == null || paths.isEmpty()) {
+ return;
+ }
try {
- String normalizedPath = normalizePathForJGit(path);
- if (normalizedPath == null || ".".equals(normalizedPath)) {
- normalizedPath = "";
- }
- CleanCommand cleanCommand = git.clean();
- if (!normalizedPath.isEmpty()) {
- cleanCommand.setPaths(Collections.singleton(normalizedPath));
+ Set<String> untracked = git.status().call().getUntracked();
+ File workTree = new File(directory);
+ for (String path : paths) {
+ String normalizedPath = normalizePathForJGit(path);
+ if (normalizedPath == null || !untracked.contains(normalizedPath)) {
+ continue;
+ }
+ File file = new File(workTree, normalizedPath);
+ org.apache.commons.io.FileUtils.deleteQuietly(file);
+ deleteEmptyParentFolders(file.getParentFile(), workTree);
}
- cleanCommand.setCleanDirectories(true).setForce(true).call();
} catch (Exception e) {
- throw new HopException("Git: error cleaning path '" + path + "'", e);
+ throw new HopException("Git: error cleaning untracked files", e);
+ }
+ }
+
+ /** Remove the folders a clean left behind empty, up to (excluding) the repository root. */
+ private void deleteEmptyParentFolders(File folder, File workTree) {
+ File parent = folder;
+ while (parent != null
+ && parent.isDirectory()
+ && !parent.equals(workTree)
+ && parent.toPath().startsWith(workTree.toPath())
+ && parent.delete()) { // only succeeds for empty folders
+ parent = parent.getParentFile();
}
}
@@ -974,14 +999,14 @@ public class UIGit extends VCS {
}
/**
- * Get the subset of revert-path files that will be deleted by revert (untracked or added). For
- * these files revert removes the file; for others (changed, missing, uncommitted) the file stays
- * and only content is reset.
+ * Get the subset of revert-path files which are new: untracked or added but not in HEAD. Revert
+ * only unstages these and leaves them on disk; removing them takes an explicit git clean. For all
+ * other files (changed, missing, uncommitted) revert restores the content from HEAD.
*
* @param path The path to revert (same as for getRevertPathFiles)
- * @return Paths that will be deleted (untracked + added)
+ * @return The new (untracked + added) paths
*/
- public Set<String> getRevertPathFilesThatWillBeDeleted(String path) throws HopException {
+ public Set<String> getNewRevertPathFiles(String path) throws HopException {
try {
Set<String> files = new HashSet<>();
String normalizedPath = normalizePathForJGit(path);
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 aa456a7190..28d564f06b 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
@@ -77,7 +77,7 @@ GitGuiPlugin.Dialog.Tag.CreateTag.Message=Enter the name of the new tag
GitGuiPlugin.Dialog.Tag.DeleteTagConfirmation.Header=Delete a Tag
GitGuiPlugin.Dialog.Tag.DeleteTagConfirmation.Message=Are you sure you want to delete tag ''{0}'' ?
GitGuiPlugin.Dialog.CleanConfirm.Header=Git Clean
-GitGuiPlugin.Dialog.CleanConfirm.Message=Should we also run git clean to remove untracked files in this folder and subfolders?
+GitGuiPlugin.Dialog.CleanConfirm.Message=The new files were unstaged and are still on disk. Should we also run git clean to remove them?
GitGuiPlugin.Dialog.SelectFilesToCommit.Header=Commit Message
GitGuiPlugin.Dialog.SelectFilesToCommit.Message=Please enter a commit message:
GitGuiPlugin.Dialog.StageFiles.Header=Select files to commit
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 dcbe60aab5..da33524cd8 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
@@ -36,12 +36,14 @@ import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.hop.git.model.revision.ObjectRevision;
import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RemoteAddCommand;
+import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -467,6 +469,104 @@ public class UIGitTest extends RepositoryTestCase {
assertEquals("Hello world", FileUtils.readFileToString(file, StandardCharsets.UTF_8));
}
+ @Test
+ public void testRevertPathOnlyUnstagesAddedFile() throws Exception {
+ initialCommit();
+
+ // A new file which was staged with "add"
+ File file = writeTrashFile("New.txt", "Hello world");
+ git.add().addFilepattern("New.txt").call();
+ assertTrue(git.status().call().getAdded().contains("New.txt"));
+
+ uiGit.revertPath("New.txt");
+
+ // The file is unstaged but is still on disk with its content intact
+ assertTrue(file.exists());
+ assertEquals("Hello world", FileUtils.readFileToString(file, StandardCharsets.UTF_8));
+ Status status = git.status().call();
+ assertTrue(status.getAdded().isEmpty());
+ assertTrue(status.getUntracked().contains("New.txt"));
+ }
+
+ @Test
+ public void testRevertPathKeepsUntrackedFile() throws Exception {
+ initialCommit();
+
+ File file = writeTrashFile("Untracked.txt", "Hello world");
+
+ uiGit.revertPath("Untracked.txt");
+
+ assertTrue(file.exists());
+ assertEquals("Hello world", FileUtils.readFileToString(file, StandardCharsets.UTF_8));
+ assertTrue(git.status().call().getUntracked().contains("Untracked.txt"));
+ }
+
+ @Test
+ public void testRevertPathRestoresDeletedFile() throws Exception {
+ File file = writeTrashFile("Test.txt", "Hello world");
+ git.add().addFilepattern("Test.txt").call();
+ git.commit().setMessage("initial commit").call();
+
+ assertTrue(file.delete());
+
+ uiGit.revertPath("Test.txt");
+
+ assertTrue(file.exists());
+ assertEquals("Hello world", FileUtils.readFileToString(file, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testGetNewRevertPathFiles() throws Exception {
+ File tracked = writeTrashFile("folder/Tracked.txt", "Hello world");
+ git.add().addFilepattern("folder/Tracked.txt").call();
+ git.commit().setMessage("initial commit").call();
+
+ FileUtils.writeStringToFile(tracked, "Change", StandardCharsets.UTF_8);
+ writeTrashFile("folder/Untracked.txt", "Untracked");
+ writeTrashFile("folder/Added.txt", "Added");
+ git.add().addFilepattern("folder/Added.txt").call();
+
+ Set<String> newFiles = uiGit.getNewRevertPathFiles("folder");
+
+ assertEquals(2, newFiles.size());
+ assertTrue(newFiles.contains("folder/Untracked.txt"));
+ assertTrue(newFiles.contains("folder/Added.txt"));
+ }
+
+ @Test
+ public void testCleanPathsOnlyRemovesUntrackedFiles() throws Exception {
+ File tracked = writeTrashFile("folder/Tracked.txt", "Hello world");
+ git.add().addFilepattern("folder/Tracked.txt").call();
+ git.commit().setMessage("initial commit").call();
+ FileUtils.writeStringToFile(tracked, "Change", StandardCharsets.UTF_8);
+
+ File untracked = writeTrashFile("folder/Untracked.txt", "Untracked");
+ File nested = writeTrashFile("folder/new-folder/Nested.txt", "Nested");
+
+ uiGit.cleanPaths(
+ List.of("folder/Untracked.txt", "folder/new-folder/Nested.txt", "folder/Tracked.txt"));
+
+ // Untracked files are removed, along with the folder they left behind empty
+ assertFalse(untracked.exists());
+ assertFalse(nested.exists());
+ assertFalse(nested.getParentFile().exists());
+
+ // A tracked file is never removed by a clean, even when it's passed in
+ assertTrue(tracked.exists());
+ assertEquals("Change", FileUtils.readFileToString(tracked, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testCleanPathsWithoutPathsDoesNotRemoveAnything() throws Exception {
+ initialCommit();
+ File untracked = writeTrashFile("Untracked.txt", "Untracked");
+
+ uiGit.cleanPaths(List.of());
+ uiGit.cleanPaths(null);
+
+ assertTrue(untracked.exists());
+ }
+
@Test
public void testCreateDeleteBranchTag() throws Exception {
initialCommit();