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 20:13:00 UTC

(hop) branch main updated: add git clean #7774 (#7805)

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 3e3a58d405 add git clean #7774 (#7805)
3e3a58d405 is described below

commit 3e3a58d405b24b857e1f3969863f4d9bf51c9603
Author: Bart Maertens <ba...@know.bi>
AuthorDate: Thu Aug 6 22:12:54 2026 +0200

    add git clean #7774 (#7805)
---
 .../main/java/org/apache/hop/git/GitGuiPlugin.java | 222 ++++++++++++++++-----
 .../main/java/org/apache/hop/git/model/UIGit.java  |  26 ++-
 .../hop/git/messages/messages_en_US.properties     |  12 +-
 .../java/org/apache/hop/git/model/UIGitTest.java   |  51 +++++
 4 files changed, 262 insertions(+), 49 deletions(-)

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 4aac3ca64d..ff9626580e 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
@@ -30,6 +30,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import lombok.Getter;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.FileSystemException;
@@ -71,6 +72,7 @@ import org.apache.hop.ui.hopgui.perspective.explorer.IExplorerRootChangedListene
 import org.apache.hop.ui.hopgui.perspective.explorer.IExplorerSelectionListener;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.apache.hop.workflow.WorkflowMeta;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.graphics.Color;
@@ -105,6 +107,7 @@ public class GitGuiPlugin
   public static final String TOOLBAR_ITEM_GIT_INFO = "ExplorerPerspective-Toolbar-20100-GitInfo";
   public static final String TOOLBAR_ITEM_ADD = "ExplorerPerspective-Toolbar-20200-Add";
   public static final String TOOLBAR_ITEM_REVERT = "ExplorerPerspective-Toolbar-20300-Revert";
+  public static final String TOOLBAR_ITEM_CLEAN = "ExplorerPerspective-Toolbar-20400-Clean";
   public static final String TOOLBAR_ITEM_COMMIT = "ExplorerPerspective-Toolbar-21000-Commit";
 
   public static final String CONTEXT_MENU_GIT_INFO =
@@ -112,6 +115,8 @@ public class GitGuiPlugin
   public static final String CONTEXT_MENU_GIT_ADD = "ExplorerPerspective-ContextMenu-20100-GitAdd";
   public static final String CONTEXT_MENU_GIT_REVERT =
       "ExplorerPerspective-ContextMenu-20200-GitRevert";
+  public static final String CONTEXT_MENU_GIT_CLEAN =
+      "ExplorerPerspective-ContextMenu-20300-GitClean";
   public static final String CONTEXT_MENU_GIT_COMMIT =
       "ExplorerPerspective-ContextMenu-21000-GitCommit";
 
@@ -403,6 +408,7 @@ public class GitGuiPlugin
       // Refresh the tree, change colors...
       //
       ExplorerPerspective.getInstance().refresh();
+      enableButtons();
     } catch (Exception e) {
       new ErrorDialog(
           HopGui.getInstance().getShell(),
@@ -465,56 +471,19 @@ public class GitGuiPlugin
           }
 
           // New files (untracked or added) are only unstaged by the revert, they stay on disk.
-          // Removing those takes an explicit git clean, see below.
+          // Removing those is what "Git Clean" is for.
           //
-          Set<String> newPaths = git.getNewRevertPathFiles(relativePath);
-
           for (String filePath : selectedPaths) {
             git.revertPath(filePath);
           }
 
-          // 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
-          }
-          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);
-            cleanBox.setText(
-                BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanConfirm.Header"));
-            cleanBox.setMessage(
-                BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanConfirm.Message"));
-            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()) {
+            String fullFilename = getOpenFilename(filePath);
+            if (new File(git.getDirectory(), filePath).exists()) {
               filenamesToReload.add(fullFilename);
             } else {
               filenamesToClose.add(fullFilename);
@@ -550,6 +519,122 @@ public class GitGuiPlugin
     enableButtons();
   }
 
+  @GuiMenuElement(
+      root = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      parentId = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      id = CONTEXT_MENU_GIT_CLEAN,
+      label = "i18n::GitGuiPlugin.Menu.Clean.Text",
+      image = "git-delete.svg")
+  @GuiToolbarElement(
+      root = ExplorerPerspective.GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ITEM_CLEAN,
+      toolTip = "i18n::GitGuiPlugin.Toolbar.Clean.Tooltip",
+      image = "git-delete.svg")
+  public void gitClean() {
+    try {
+      ExplorerFile explorerFile = getSelectedFile();
+      if (git == null || explorerFile == null) {
+        return;
+      }
+      String relativePath = calculateRelativePath(git.getDirectory(), explorerFile);
+      if (relativePath == null) {
+        return;
+      }
+      List<String> untrackedFiles = git.getUntrackedPathFiles(relativePath);
+      if (untrackedFiles.isEmpty()) {
+        MessageBox box =
+            new MessageBox(HopGui.getInstance().getShell(), SWT.OK | SWT.ICON_INFORMATION);
+        box.setText(BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.NoFilesToClean.Header"));
+        box.setMessage(BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.NoFilesToClean.Message"));
+        box.open();
+      } else {
+        String[] files = untrackedFiles.toArray(new String[0]);
+        int[] selectedIndexes = new int[files.length];
+        for (int i = 0; i < files.length; i++) {
+          selectedIndexes[i] = i;
+        }
+        EnterSelectionDialog selectionDialog =
+            new EnterSelectionDialog(
+                HopGui.getInstance().getShell(),
+                files,
+                BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanFiles.Header"),
+                BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanFiles.Message"));
+        selectionDialog.setMulti(true);
+        // Select all files by default
+        //
+        selectionDialog.setSelectedNrs(selectedIndexes);
+        String selection = selectionDialog.open();
+        if (selection != null) {
+          List<String> pathsToClean = new ArrayList<>();
+          for (int selectedNr : selectionDialog.getSelectionIndeces()) {
+            pathsToClean.add(files[selectedNr]);
+          }
+
+          // Look up the filenames of the open tabs before the files are gone
+          //
+          Map<String, String> filenames = new HashMap<>();
+          for (String filePath : pathsToClean) {
+            filenames.put(filePath, getOpenFilename(filePath));
+          }
+
+          git.cleanPaths(pathsToClean);
+
+          // Close the tabs of the files which were deleted
+          //
+          List<String> filenamesToClose = new ArrayList<>();
+          for (String filePath : pathsToClean) {
+            if (!new File(git.getDirectory(), filePath).exists()) {
+              filenamesToClose.add(filenames.get(filePath));
+            }
+          }
+          ExplorerPerspective.getInstance().closeTabsForFilenames(filenamesToClose);
+
+          // Show confirmation message once after all files have been deleted
+          MessageBox box =
+              new MessageBox(HopGui.getInstance().getShell(), SWT.OK | SWT.ICON_INFORMATION);
+          box.setText(BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.FilesCleaned.Header"));
+          box.setMessage(BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.FilesCleaned.Message"));
+          box.open();
+        }
+      }
+    } catch (Exception e) {
+      new ErrorDialog(
+          HopGui.getInstance().getShell(),
+          BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanError.Header"),
+          BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.CleanError.Message"),
+          e);
+    }
+
+    // Refresh the git history, file explorer tree, change colors...
+    //
+    // TODO: To remove when git perspective work on web
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      ExplorerPerspective.getInstance().refresh();
+    } else {
+      GitPerspective.getInstance().refresh(true);
+    }
+    enableButtons();
+  }
+
+  /**
+   * Calculate the filename an open editor tab uses for a file in the repository.
+   *
+   * @param relativePath The path of the file, relative to the repository root
+   * @return The filename to match open tabs with
+   */
+  private String getOpenFilename(String relativePath) {
+    File file = new File(git.getDirectory(), relativePath);
+    try {
+      FileObject fileObject = HopVfs.getFileObject(file.getAbsolutePath());
+      if (fileObject.exists()) {
+        return HopVfs.getFilename(fileObject);
+      }
+    } catch (Exception ignored) {
+      // Fall back to the absolute filename below
+    }
+    return file.getAbsolutePath();
+  }
+
   @GuiMenuElement(
       root = ID_CONTEXT_MENU_GIT,
       parentId = ID_CONTEXT_MENU_GIT,
@@ -845,6 +930,8 @@ public class GitGuiPlugin
   @Override
   public void beforeRefresh() {
     refreshChangedFiles();
+    // The git state of the files determines which operations are available
+    enableButtons();
   }
 
   @Override
@@ -857,21 +944,64 @@ public class GitGuiPlugin
     boolean isGit = git != null;
     boolean isSelected = isGit && getSelectedFile() != null;
 
+    // Only offer the git operations which make sense for what is selected:
+    //
+    // - Add stages what isn't staged yet
+    // - Commit needs any change at all, staged or not
+    // - Revert restores files from HEAD and unstages new files, so it needs something which isn't
+    //   simply untracked
+    // - Clean deletes untracked files, so it needs the opposite of revert
+    //
+    boolean canAdd = isSelected && selectionContains(file -> !file.isStaged());
+    boolean canCommit = isSelected && selectionContains(file -> true);
+    boolean canRevert = isSelected && selectionContains(file -> !isUntracked(file));
+    boolean canClean = isSelected && selectionContains(GitGuiPlugin::isUntracked);
+
     GuiToolbarWidgets toolBarWidgets = ExplorerPerspective.getInstance().getToolBarWidgets();
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_GIT_INFO, isGit);
-    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_ADD, isSelected);
-    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_REVERT, isSelected);
-    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_COMMIT, isSelected);
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_ADD, canAdd);
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_REVERT, canRevert);
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CLEAN, canClean);
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_COMMIT, canCommit);
 
     GuiMenuWidgets menuWidgets = ExplorerPerspective.getInstance().getMenuWidgets();
     menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_INFO, isGit);
-    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_ADD, isSelected);
-    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_COMMIT, isSelected);
-    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_REVERT, isSelected);
+    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_ADD, canAdd);
+    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_COMMIT, canCommit);
+    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_REVERT, canRevert);
+    menuWidgets.enableMenuItem(CONTEXT_MENU_GIT_CLEAN, canClean);
 
     HopGui.getInstance().getStatusToolbarWidgets().enableToolbarItem(ID_TOOLBAR_ITEM_GIT, isGit);
   }
 
+  /** An untracked file: git doesn't know about it, so only a clean can remove it. */
+  private static boolean isUntracked(UIFile file) {
+    return file.getChangeType() == ChangeType.ADD && !file.isStaged();
+  }
+
+  /**
+   * Check the changed files of the selected file or folder against a condition. For a folder all
+   * the changed files below it are taken into account.
+   *
+   * @param condition The condition to check
+   * @return true if at least one changed file in the selection matches the condition
+   */
+  private boolean selectionContains(Predicate<UIFile> condition) {
+    ExplorerFile explorerFile = getSelectedFile();
+    if (explorerFile == null || changedFiles == null) {
+      return false;
+    }
+    String selectedPath = getAbsoluteFilename(explorerFile.getFilename());
+    for (Map.Entry<String, UIFile> entry : changedFiles.entrySet()) {
+      String path = entry.getKey();
+      if ((path.equals(selectedPath) || path.startsWith(selectedPath + "/"))
+          && condition.test(entry.getValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * If we have a git project we can take a look and see if a file is changed.
    *
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 e97965ebab..1423571820 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
@@ -444,6 +444,8 @@ public class UIGit extends VCS {
     }
     status.getUntracked().forEach(name -> files.add(new UIFile(name, ChangeType.ADD, false)));
     status.getConflicting().forEach(name -> files.add(new UIFile(name, ChangeType.MODIFY, false)));
+    // Changed in the working tree but not staged: "git add" is what stages these
+    status.getModified().forEach(name -> files.add(new UIFile(name, ChangeType.MODIFY, false)));
     status.getMissing().forEach(name -> files.add(new UIFile(name, ChangeType.DELETE, false)));
     return files;
   }
@@ -457,9 +459,9 @@ public class UIGit extends VCS {
       e.printStackTrace();
       return files;
     }
+    // Only what is in the index: working tree changes are reported by getUnstagedFiles()
     status.getAdded().forEach(name -> files.add(new UIFile(name, ChangeType.ADD, true)));
     status.getChanged().forEach(name -> files.add(new UIFile(name, ChangeType.MODIFY, true)));
-    status.getModified().forEach(name -> files.add(new UIFile(name, ChangeType.MODIFY, true)));
     status.getRemoved().forEach(name -> files.add(new UIFile(name, ChangeType.DELETE, true)));
     return files;
   }
@@ -998,6 +1000,28 @@ public class UIGit extends VCS {
     }
   }
 
+  /**
+   * Get the untracked files under the given path: the files a git clean would remove. Ignored files
+   * are not part of this list, git clean leaves those alone as well.
+   *
+   * @param path The path to clean (relative to the repository root)
+   * @return The untracked files, sorted by name
+   */
+  public List<String> getUntrackedPathFiles(String path) throws HopException {
+    try {
+      String normalizedPath = normalizePathForJGit(path);
+      StatusCommand statusCommand = git.status();
+      if (normalizedPath != null && !".".equals(normalizedPath)) {
+        statusCommand = statusCommand.addPath(normalizedPath);
+      }
+      List<String> files = new ArrayList<>(statusCommand.call().getUntracked());
+      Collections.sort(files);
+      return files;
+    } catch (Exception e) {
+      throw new HopException("Git: error getting untracked files for '" + path + "'", e);
+    }
+  }
+
   /**
    * 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
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 28d564f06b..ddebb027d4 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
@@ -42,10 +42,18 @@ GitGuiPlugin.Dialog.CherryPickCommitRepositoryIsNotClean.Message=Please clean yo
 GitGuiPlugin.Dialog.CherryPickCommitConflicts.Message=There were conflicts while cherry-picking commit ''{0}''\n\n{1}
 GitGuiPlugin.Dialog.CommitError.Header=Commit Error
 GitGuiPlugin.Dialog.CommitError.Message=There was an error doing a git commit
+GitGuiPlugin.Dialog.CleanError.Header=Clean Error
+GitGuiPlugin.Dialog.CleanError.Message=There was an error doing a git clean
+GitGuiPlugin.Dialog.CleanFiles.Header=Select files to delete
+GitGuiPlugin.Dialog.CleanFiles.Message=These files are not tracked by git. Please select the files to delete from disk.
+GitGuiPlugin.Dialog.FilesCleaned.Header=Files Deleted
+GitGuiPlugin.Dialog.FilesCleaned.Message=The selected untracked files were deleted
 GitGuiPlugin.Dialog.FilesReverted.Header=Files Reverted
 GitGuiPlugin.Dialog.FilesReverted.Message=Files Reverted
 GitGuiPlugin.Dialog.MergeBranchError.Header=Error merging branch
 GitGuiPlugin.Dialog.MergeBranchError.Message=Error merging branch
+GitGuiPlugin.Dialog.NoFilesToClean.Header=No Files to Delete
+GitGuiPlugin.Dialog.NoFilesToClean.Message=Sorry, there are no untracked files to delete
 GitGuiPlugin.Dialog.NoFilesToCommit.Header=No Files to Commit
 GitGuiPlugin.Dialog.NoFilesToCommit.Message=Sorry, there are no files to commit
 GitGuiPlugin.Dialog.NoFilesToRevert.Header=No Files to Revert
@@ -76,8 +84,6 @@ GitGuiPlugin.Dialog.Tag.CreateTag.Header=Create a Tag
 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=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
@@ -96,6 +102,7 @@ GitGuiPlugin.Menu.Branch.Rename.Text=Rename branch...
 GitGuiPlugin.Menu.Commit.Text=Git Commit
 GitGuiPlugin.Menu.Info.Text=Git Info
 GitGuiPlugin.Menu.Revert.Text=Git Revert
+GitGuiPlugin.Menu.Clean.Text=Git Clean
 GitGuiPlugin.Menu.Restore.Text=Git Restore
 GitGuiPlugin.Menu.ShowDiff.Text=Show diff
 GitGuiPlugin.Project.Label=Git Project
@@ -107,6 +114,7 @@ GitGuiPlugin.Toolbar.Fetch.Tooltip=Fetch
 GitGuiPlugin.Toolbar.Pull.Tooltip=Git Pull
 GitGuiPlugin.Toolbar.Push.Tooltip=Git Push
 GitGuiPlugin.Toolbar.Revert.Tooltip=Git revert file to the last committed version (HEAD)
+GitGuiPlugin.Toolbar.Clean.Tooltip=Git clean: delete untracked files from disk
 
 GitCommitPerspective.Description=Git commit perspective
 GitCommitPerspective.Name=Git 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 da33524cd8..670baccdba 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
@@ -533,6 +533,57 @@ public class UIGitTest extends RepositoryTestCase {
     assertTrue(newFiles.contains("folder/Added.txt"));
   }
 
+  @Test
+  public void testModifiedFilesAreUnstagedUntilTheyAreAdded() throws Exception {
+    File file = writeTrashFile("Test.txt", "Hello world");
+    git.add().addFilepattern("Test.txt").call();
+    git.commit().setMessage("initial commit").call();
+
+    // A change in the working tree is not staged, only "git add" stages it
+    FileUtils.writeStringToFile(file, "Change", StandardCharsets.UTF_8);
+
+    List<UIFile> unstaged = uiGit.getUnstagedFiles();
+    assertEquals(1, unstaged.size());
+    assertEquals("Test.txt", unstaged.get(0).getName());
+    assertEquals(ChangeType.MODIFY, unstaged.get(0).getChangeType());
+    assertFalse(unstaged.get(0).isStaged());
+    assertTrue(uiGit.getStagedFiles().isEmpty());
+
+    uiGit.add("Test.txt");
+
+    List<UIFile> staged = uiGit.getStagedFiles();
+    assertEquals(1, staged.size());
+    assertEquals("Test.txt", staged.get(0).getName());
+    assertEquals(ChangeType.MODIFY, staged.get(0).getChangeType());
+    assertTrue(staged.get(0).isStaged());
+    assertTrue(uiGit.getUnstagedFiles().isEmpty());
+  }
+
+  @Test
+  public void testGetUntrackedPathFiles() throws Exception {
+    File tracked = writeTrashFile("folder/Tracked.txt", "Hello world");
+    writeTrashFile("folder/Ignored.txt", "Ignored");
+    writeTrashFile(".gitignore", "Ignored.txt");
+    git.add().addFilepattern("folder/Tracked.txt").addFilepattern(".gitignore").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();
+    writeTrashFile("outside/Other.txt", "Outside the folder");
+
+    // Only the untracked file: not the tracked, added, ignored or out of scope ones
+    assertEquals(List.of("folder/Untracked.txt"), uiGit.getUntrackedPathFiles("folder"));
+
+    // A single file works as well, as does the whole repository
+    assertEquals(
+        List.of("folder/Untracked.txt"), uiGit.getUntrackedPathFiles("folder/Untracked.txt"));
+    assertTrue(uiGit.getUntrackedPathFiles("folder/Tracked.txt").isEmpty());
+    assertEquals(
+        List.of("folder/Untracked.txt", "outside/Other.txt"), uiGit.getUntrackedPathFiles(null));
+  }
+
   @Test
   public void testCleanPathsOnlyRemovesUntrackedFiles() throws Exception {
     File tracked = writeTrashFile("folder/Tracked.txt", "Hello world");