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();