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 10cfb775e9 add Untracked tree and file counts per tree. fixes #7901
(#7904)
10cfb775e9 is described below
commit 10cfb775e9b959a51ffa72b70f6fa3fc2373bad1
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Aug 13 10:33:00 2026 +0200
add Untracked tree and file counts per tree. fixes #7901 (#7904)
---
.../modules/ROOT/pages/hop-gui/hop-gui-git.adoc | 14 +-
.../org/apache/hop/git/GitCommitPerspective.java | 147 ++++++++++++-------
.../hop/git/model/CaseInsensitiveIgnores.java | 156 +++++++++++++++++++++
.../main/java/org/apache/hop/git/model/UIGit.java | 30 +++-
.../hop/git/messages/messages_en_US.properties | 5 +-
.../hop/git/messages/messages_pt_BR.properties | 4 +-
.../java/org/apache/hop/git/model/UIGitTest.java | 65 +++++++++
7 files changed, 358 insertions(+), 63 deletions(-)
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-git.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-git.adoc
index f7734e8255..b0350f7105 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-git.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-git.adoc
@@ -99,18 +99,22 @@ This is recognised as long as the rename is all that
happened to it, a move asid
== Git Commit Perspective
-The git commit perspective (`CTRL+SHIFT+O`, `CMD+SHIFT+O` on macOS) lists all
files with changes in two groups: 'Staged' (added to git's index with 'Add')
and 'Unstaged' (everything else, including files git doesn't track yet).
+The git commit perspective (`CTRL+SHIFT+O`, `CMD+SHIFT+O` on macOS) lists your
changes in the same three groups `git status` reports:
+
+Staged:: added to git's index with 'Add', part of the next commit.
+Unstaged:: changed files that git tracks, but the changes are not in the index.
+Untracked:: files git doesn't know about yet, because they were never added.
The toolbar and right-click options act on the files whose checkbox is ticked,
not on the highlighted row.
-Files git already knows about are ticked for you, so a commit takes your work
in progress along: untick a file (or unstage it) to leave it out.
-Files that are not tracked yet are listed but never ticked: committing a file
for the first time is something you decide, tick it yourself to include it.
+Staged and unstaged files are ticked for you, so a commit takes your work in
progress along: untick a file (or unstage it) to leave it out.
+Untracked files are listed but never ticked: committing a file for the first
time is something you decide, tick it yourself to include it.
[options="header", cols="1,2,5,1"]
|===
|icon|action|description|shortcut
|image:icons/refresh.svg[width="24px"]|Refresh|Reload the list of staged and
unstaged files.|F5
-|image:icons/git-add.svg[width="24px"]|Add|Stage the checked files. They move
to 'Staged' and become part of the next commit.|CTRL+ALT+A
-|image:icons/git-unstage.svg[width="24px"]|Unstage|Take the checked files out
of the next commit. They move back to 'Unstaged', your changes are
kept.|CTRL+ALT+U
+|image:icons/git-add.svg[width="24px"]|Add|Stage the checked unstaged or
untracked files. They move to 'Staged' and become part of the next commit. For
an untracked file this is also what puts it under version control.|CTRL+ALT+A
+|image:icons/git-unstage.svg[width="24px"]|Unstage|Take the checked files out
of the next commit. They move back to 'Unstaged' (or to 'Untracked' if they
were never committed before), your changes are kept.|CTRL+ALT+U
|image:icons/git-restore.svg[width="24px"]|Restore|Throw the changes in the
checked files away and restore the last committed version (HEAD). Asks for
confirmation.|CTRL+ALT+Z
|image:icons/delete.svg[width="24px"]|Delete|Delete the checked files from
disk.|DEL
|===
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 928dfd5530..df397808ca 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
@@ -116,12 +116,18 @@ public class GitCommitPerspective implements
IHopPerspective {
public static final String TOOLBAR_ITEM_DELETE =
"GitCommitPerspective-Toolbar-10400-Delete";
private static final String COMMIT_MESSAGES_AUDIT_TYPE = "commit-messages";
+ private static final String STAGED_LABEL =
"GitCommitPerspective.Status.Staged.Label";
+ private static final String UNSTAGED_LABEL =
"GitCommitPerspective.Status.Unstaged.Label";
+ private static final String UNTRACKED_LABEL =
"GitCommitPerspective.Status.Untracked.Label";
@Getter private static GitCommitPerspective instance;
private HopGui hopGui;
private SashForm wSashForm;
private Control wToolBar;
private Tree wTree;
+ private TreeItem stagedRootItem;
+ private TreeItem unstagedRootItem;
+ private TreeItem untrackedRootItem;
private Text wMessage;
private CLabel wStatus;
private Button wAmend;
@@ -258,7 +264,7 @@ public class GitCommitPerspective implements
IHopPerspective {
event -> {
List<UIFile> selectedFiles = this.getSelectedFiles();
- setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD,
!getSelectedUntrackedFiles().isEmpty());
+ setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD,
!getSelectedUnstagedFiles().isEmpty());
setMenuItemEnabled(
menuWidgets, CONTEXT_MENU_UNSTAGE,
!getSelectedStagedFiles().isEmpty());
setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD_TO_GIT_IGNORE,
!selectedFiles.isEmpty());
@@ -348,18 +354,19 @@ public class GitCommitPerspective implements
IHopPerspective {
* committing a file for the first time is a deliberate act, git doesn't do
it for you either.
*/
private void checkChangedFiles() {
- if (wTree == null || wTree.isDisposed() || wTree.getItemCount() < 2) {
+ setChecked(stagedRootItem, true);
+ setChecked(unstagedRootItem, true);
+ setChecked(untrackedRootItem, false);
+ }
+
+ /** Check or uncheck a whole group of files. */
+ private void setChecked(TreeItem rootItem, boolean checked) {
+ if (!isUsable(rootItem)) {
return;
}
-
- for (TreeItem rootItem : wTree.getItems()) {
- boolean allChecked = rootItem.getItemCount() > 0;
- for (TreeItem item : rootItem.getItems()) {
- boolean tracked = !(item.getData() instanceof UIFile file) ||
!isUntracked(file);
- item.setChecked(tracked);
- allChecked = allChecked && tracked;
- }
- rootItem.setChecked(allChecked);
+ rootItem.setChecked(checked && rootItem.getItemCount() > 0);
+ for (TreeItem item : rootItem.getItems()) {
+ item.setChecked(checked);
}
}
@@ -368,21 +375,28 @@ public class GitCommitPerspective implements
IHopPerspective {
return !file.isStaged() && file.getChangeType() ==
DiffEntry.ChangeType.ADD;
}
- /** Uncheck the given files under the unstaged node, they are no longer part
of the commit. */
+ /**
+ * Uncheck the given files wherever they ended up outside the index: they
are no longer part of
+ * the commit. A file that was staged as new lands under "Untracked" once it
is unstaged.
+ */
private void uncheckUnstagedFiles(Set<String> fileNames) {
- if (wTree == null || wTree.isDisposed() || wTree.getItemCount() < 2 ||
fileNames.isEmpty()) {
+ if (fileNames.isEmpty()) {
return;
}
- TreeItem unstagedRootItem = this.getUnstagedRootItem();
- boolean allChecked = true;
- for (TreeItem item : unstagedRootItem.getItems()) {
- if (item.getData() instanceof UIFile file &&
fileNames.contains(file.getName())) {
- item.setChecked(false);
+ for (TreeItem rootItem : new TreeItem[] {unstagedRootItem,
untrackedRootItem}) {
+ if (!isUsable(rootItem)) {
+ continue;
+ }
+ boolean allChecked = rootItem.getItemCount() > 0;
+ for (TreeItem item : rootItem.getItems()) {
+ if (item.getData() instanceof UIFile file &&
fileNames.contains(file.getName())) {
+ item.setChecked(false);
+ }
+ allChecked = allChecked && item.getChecked();
}
- allChecked = allChecked && item.getChecked();
+ rootItem.setChecked(allChecked);
}
- unstagedRootItem.setChecked(allChecked && unstagedRootItem.getItemCount()
> 0);
}
protected void retrieveState() {
@@ -809,7 +823,9 @@ public class GitCommitPerspective implements
IHopPerspective {
// Unselected staged files
List<UIFile> filesToIgnore =
- this.getSelectedFiles(getStagedRootItem().getItems(), new
ArrayList<>(), false);
+ isUsable(stagedRootItem)
+ ? this.getSelectedFiles(stagedRootItem.getItems(), new
ArrayList<>(), false)
+ : new ArrayList<>();
// No files to commit. Selecting a row in the tree only highlights it,
so tell the user to
// check the box when there are files listed but none of them are
checked.
@@ -920,12 +936,36 @@ public class GitCommitPerspective implements
IHopPerspective {
PKG, "GitCommitPerspective.Status.CommitId.Label",
commitId.substring(0, 7));
}
- private TreeItem getStagedRootItem() {
- return wTree.getItems()[0];
+ /** Create one of the group nodes the files are listed under. */
+ private TreeItem createRootItem(String labelKey) {
+ TreeItem rootItem = new TreeItem(wTree, SWT.NONE);
+ rootItem.setImage(GuiResource.getInstance().getImageFolder());
+ setRootItemLabel(rootItem, labelKey);
+ return rootItem;
+ }
+
+ /**
+ * Label a group node with the number of files in it, so a collapsed group
still tells you how
+ * much is in there.
+ */
+ private void setRootItemLabel(TreeItem rootItem, String labelKey) {
+ if (!isUsable(rootItem)) {
+ return;
+ }
+ rootItem.setText(
+ BaseMessages.getString(PKG, labelKey,
Integer.toString(rootItem.getItemCount())));
+ }
+
+ /** The group a file belongs to, the way "git status" reports it. */
+ private TreeItem getRootItemFor(UIFile file) {
+ if (file.isStaged()) {
+ return stagedRootItem;
+ }
+ return isUntracked(file) ? untrackedRootItem : unstagedRootItem;
}
- private TreeItem getUnstagedRootItem() {
- return wTree.getItems()[1];
+ private boolean isUsable(TreeItem rootItem) {
+ return wTree != null && !wTree.isDisposed() && rootItem != null &&
!rootItem.isDisposed();
}
protected List<UIFile> getSelectedFiles() {
@@ -936,23 +976,24 @@ public class GitCommitPerspective implements
IHopPerspective {
}
protected List<UIFile> getSelectedStagedFiles() {
- if (wTree == null || wTree.isDisposed()) {
- return List.of();
- }
- if (wTree.getItemCount() == 0) {
+ if (!isUsable(stagedRootItem)) {
return List.of();
}
- return getSelectedFiles(getStagedRootItem().getItems(), new ArrayList<>(),
true);
+ return getSelectedFiles(stagedRootItem.getItems(), new ArrayList<>(),
true);
}
- protected List<UIFile> getSelectedUntrackedFiles() {
- if (wTree == null || wTree.isDisposed()) {
- return List.of();
- }
- if (wTree.getItemCount() == 0) {
- return List.of();
+ /**
+ * The checked files which are not in the index: changed files git tracks
and files it doesn't
+ * know about yet. These are the ones "Add" can stage.
+ */
+ protected List<UIFile> getSelectedUnstagedFiles() {
+ List<UIFile> files = new ArrayList<>();
+ for (TreeItem rootItem : new TreeItem[] {unstagedRootItem,
untrackedRootItem}) {
+ if (isUsable(rootItem)) {
+ getSelectedFiles(rootItem.getItems(), files, true);
+ }
}
- return getSelectedFiles(getUnstagedRootItem().getItems(), new
ArrayList<>(), true);
+ return files;
}
protected List<UIFile> getUncheckedFiles() {
@@ -981,7 +1022,7 @@ public class GitCommitPerspective implements
IHopPerspective {
wAmend.setEnabled(gitEnabled);
wMessage.setEnabled(gitEnabled);
- toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_ADD,
!getSelectedUntrackedFiles().isEmpty());
+ toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_ADD,
!getSelectedUnstagedFiles().isEmpty());
toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_UNSTAGE,
!getSelectedStagedFiles().isEmpty());
toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_RESTORE,
!getSelectedStagedFiles().isEmpty());
toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_DELETE,
!getSelectedFiles().isEmpty());
@@ -1008,14 +1049,12 @@ public class GitCommitPerspective implements
IHopPerspective {
UIGit git = GitGuiPlugin.getInstance().getGit();
if (git != null) {
- TreeItem stagedItem = new TreeItem(wTree, SWT.NONE);
- stagedItem.setImage(GuiResource.getInstance().getImageFolder());
- stagedItem.setText(BaseMessages.getString(PKG,
"GitCommitPerspective.Status.Staged.Label"));
-
- TreeItem untrackedItem = new TreeItem(wTree, SWT.NONE);
- untrackedItem.setText(
- BaseMessages.getString(PKG,
"GitCommitPerspective.Status.Unstaged.Label"));
- untrackedItem.setImage(GuiResource.getInstance().getImageFolder());
+ // The three groups "git status" reports: what is in the index, what
changed in files git
+ // tracks, and the files it doesn't know about yet.
+ //
+ stagedRootItem = createRootItem(STAGED_LABEL);
+ unstagedRootItem = createRootItem(UNSTAGED_LABEL);
+ untrackedRootItem = createRootItem(UNTRACKED_LABEL);
// Reload changes files
GitGuiPlugin.getInstance().refreshChangedFiles();
@@ -1025,10 +1064,7 @@ public class GitCommitPerspective implements
IHopPerspective {
for (String fileName : filesToCommit.keySet()) {
UIFile file = filesToCommit.get(fileName);
- // What is in the index goes under "Staged", the rest under
"Unstaged", whatever the
- // type of change is. A file that gets unstaged has to move to the
unstaged node.
- //
- TreeItem item = new TreeItem(file.isStaged() ? stagedItem :
untrackedItem, SWT.NONE);
+ TreeItem item = new TreeItem(getRootItemFor(file), SWT.NONE);
switch (file.getChangeType()) {
case ADD, COPY, RENAME ->
item.setForeground(
@@ -1046,8 +1082,15 @@ public class GitCommitPerspective implements
IHopPerspective {
item.setData(file);
}
- stagedItem.setExpanded(true);
- untrackedItem.setExpanded(true);
+ // The files are in, so the groups can say how many they hold
+ //
+ setRootItemLabel(stagedRootItem, STAGED_LABEL);
+ setRootItemLabel(unstagedRootItem, UNSTAGED_LABEL);
+ setRootItemLabel(untrackedRootItem, UNTRACKED_LABEL);
+
+ for (TreeItem rootItem : wTree.getItems()) {
+ rootItem.setExpanded(true);
+ }
// Offer all the work in progress for the next commit
//
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/model/CaseInsensitiveIgnores.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/CaseInsensitiveIgnores.java
new file mode 100644
index 0000000000..2613d70d0c
--- /dev/null
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/CaseInsensitiveIgnores.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.git.model;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.hop.core.logging.LogChannel;
+import org.eclipse.jgit.ignore.FastIgnoreRule;
+import org.eclipse.jgit.ignore.IgnoreNode;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Ignore rules matched without regard to case, for repositories on a case
insensitive file system.
+ *
+ * <p>JGit matches .gitignore patterns case sensitively whatever
<code>core.ignorecase</code> says,
+ * so on macOS and Windows it reports files as untracked which git itself
ignores: a rule like
+ * <code>output/</code> does not catch a folder named <code>Output</code>.
This runs the rules of
+ * the repository again with both the patterns and the paths folded to lower
case, the way git
+ * matches them when <code>core.ignorecase</code> is on.
+ */
+class CaseInsensitiveIgnores {
+
+ /** JGit has no constant for it: git writes this when the file system
ignores case. */
+ static final String CONFIG_KEY_IGNORECASE = "ignorecase";
+
+ private final File workTree;
+
+ /** The rules that apply to the whole repository: .git/info/exclude and
core.excludesFile. */
+ private final IgnoreNode repositoryNode;
+
+ /** The .gitignore of a directory, relative to the work tree, "" being the
root. */
+ private final Map<String, IgnoreNode> directoryNodes = new HashMap<>();
+
+ /**
+ * Whether the ignore rules of this repository have to be matched without
regard to case. Only
+ * repositories that say they are on a case insensitive file system need it:
elsewhere git matches
+ * case sensitively, exactly like JGit.
+ */
+ static boolean appliesTo(Repository repository) {
+ return repository
+ .getConfig()
+ .getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
CONFIG_KEY_IGNORECASE, false);
+ }
+
+ CaseInsensitiveIgnores(Repository repository) {
+ this.workTree = repository.getWorkTree();
+
+ List<FastIgnoreRule> rules = new ArrayList<>();
+ rules.addAll(readRules(new File(repository.getDirectory(),
"info/exclude")));
+ String excludesFile =
+ repository
+ .getConfig()
+ .getString(
+ ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_EXCLUDESFILE);
+ if (excludesFile != null && !excludesFile.isBlank()) {
+ rules.addAll(readRules(new File(replaceUserHome(excludesFile))));
+ }
+ this.repositoryNode = new IgnoreNode(rules);
+ }
+
+ /**
+ * Whether git ignores this file. A file is ignored when it matches a rule
itself, or when any of
+ * the directories above it is ignored: git never descends into an ignored
directory, so nothing
+ * inside it can be brought back by a later rule.
+ *
+ * @param path the path of the file, relative to the work tree, with forward
slashes
+ */
+ boolean isIgnored(String path) {
+ for (int slash = path.indexOf('/'); slash > 0; slash = path.indexOf('/',
slash + 1)) {
+ if (Boolean.TRUE.equals(checkPath(path.substring(0, slash), true))) {
+ return true;
+ }
+ }
+ return Boolean.TRUE.equals(checkPath(path, false));
+ }
+
+ /**
+ * Ask the .gitignore files whether this path is ignored, closest one first:
the rules of a
+ * directory win from the ones above it. Null means that no rule had
anything to say about it.
+ */
+ private Boolean checkPath(String path, boolean isDirectory) {
+ String foldedPath = path.toLowerCase(Locale.ROOT);
+
+ int slash = foldedPath.lastIndexOf('/');
+ while (slash >= 0) {
+ String directory = foldedPath.substring(0, slash);
+ Boolean ignored =
+ nodeFor(directory).checkIgnored(foldedPath.substring(slash + 1),
isDirectory);
+ if (ignored != null) {
+ return ignored;
+ }
+ slash = foldedPath.lastIndexOf('/', slash - 1);
+ }
+
+ Boolean ignored = nodeFor("").checkIgnored(foldedPath, isDirectory);
+ return ignored != null ? ignored : repositoryNode.checkIgnored(foldedPath,
isDirectory);
+ }
+
+ private IgnoreNode nodeFor(String directory) {
+ return directoryNodes.computeIfAbsent(
+ directory,
+ dir ->
+ new IgnoreNode(
+ readRules(
+ new File(dir.isEmpty() ? workTree : new File(workTree,
dir), ".gitignore"))));
+ }
+
+ /** The rules of one ignore file, folded to lower case so they match paths
of any case. */
+ private List<FastIgnoreRule> readRules(File ignoreFile) {
+ List<FastIgnoreRule> rules = new ArrayList<>();
+ if (!ignoreFile.isFile()) {
+ return rules;
+ }
+ try {
+ for (String line : Files.readAllLines(ignoreFile.toPath(),
StandardCharsets.UTF_8)) {
+ String pattern = line.trim();
+ if (!pattern.isEmpty() && !pattern.startsWith("#")) {
+ rules.add(new FastIgnoreRule(pattern.toLowerCase(Locale.ROOT)));
+ }
+ }
+ } catch (Exception e) {
+ // An ignore file we can't read simply holds no rules, the same as JGit
does
+ LogChannel.UI.logDebug("Unable to read ignore file '" + ignoreFile + "':
" + e.getMessage());
+ }
+ return rules;
+ }
+
+ private static String replaceUserHome(String path) {
+ if (path.startsWith("~/")) {
+ return System.getProperty("user.home") + path.substring(1);
+ }
+ return path;
+ }
+}
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 40a86519b3..6bc41c80af 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
@@ -442,7 +442,10 @@ public class UIGit extends VCS {
e.printStackTrace();
return files;
}
- status.getUntracked().forEach(name -> files.add(new UIFile(name,
ChangeType.ADD, false)));
+ Set<String> ignored = getCaseInsensitiveIgnored(status.getUntracked());
+ status.getUntracked().stream()
+ .filter(name -> !ignored.contains(name))
+ .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)));
@@ -1495,10 +1498,33 @@ public class UIGit extends VCS {
statusCommand = statusCommand.addPath(normalizedPath);
}
Status status = statusCommand.call();
- return status.getIgnoredNotInIndex();
+ Set<String> ignored = new HashSet<>(status.getIgnoredNotInIndex());
+ ignored.addAll(getCaseInsensitiveIgnored(status.getUntracked()));
+ return ignored;
} catch (GitAPIException e) {
LogChannel.UI.logError("Error getting list of files ignored by git", e);
return new HashSet<>();
}
}
+
+ /**
+ * The files JGit reports as untracked but git itself ignores. JGit matches
ignore rules case
+ * sensitively whatever core.ignorecase says, so on a case insensitive file
system a rule like
+ * "output/" leaves everything in a folder named "Output" untracked. Empty
for repositories that
+ * are matched case sensitively, where JGit and git agree.
+ */
+ private Set<String> getCaseInsensitiveIgnored(Set<String> untrackedFiles) {
+ Repository repository = git.getRepository();
+ if (untrackedFiles.isEmpty() ||
!CaseInsensitiveIgnores.appliesTo(repository)) {
+ return Set.of();
+ }
+ CaseInsensitiveIgnores ignores = new CaseInsensitiveIgnores(repository);
+ Set<String> ignored = new HashSet<>();
+ for (String file : untrackedFiles) {
+ if (ignores.isIgnored(file)) {
+ ignored.add(file);
+ }
+ }
+ return ignored;
+ }
}
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 efb5513ab2..8e6aff175e 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
@@ -138,8 +138,9 @@ GitCommitPerspective.Error.NoFilesChecked.Message=Please
check the box in front
GitCommitPerspective.Status.Committed.Message=Committed {0} file(s) {1}
GitCommitPerspective.Status.CommittedAndPushed.Message=Committed and pushed
{0} file(s) {1}
GitCommitPerspective.Status.CommitId.Label=as {0}
-GitCommitPerspective.Status.Staged.Label=Staged
-GitCommitPerspective.Status.Unstaged.Label=Unstaged
+GitCommitPerspective.Status.Staged.Label=Staged ({0})
+GitCommitPerspective.Status.Unstaged.Label=Unstaged ({0})
+GitCommitPerspective.Status.Untracked.Label=Untracked ({0})
GitCommitPerspective.Toolbar.AddToGit.Tooltip=Stage the selected files
GitCommitPerspective.Toolbar.Delete.Tooltip=Delete...
GitCommitPerspective.Toolbar.Refresh.Tooltip=Refresh
diff --git
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_pt_BR.properties
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_pt_BR.properties
index 24d03890bc..3ca8301636 100644
---
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_pt_BR.properties
+++
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_pt_BR.properties
@@ -123,8 +123,8 @@ GitCommitPerspective.Menu.Restore.Text=Restaurar...
GitCommitPerspective.Menu.ShowTextDiff.Text=Mostrar diff de texto
GitCommitPerspective.Menu.ShowGraphDiff.Text=Mostrar diff visual
GitCommitPerspective.Menu.Delete.Text=Apagar...
-GitCommitPerspective.Status.Staged.Label=Estágio
-GitCommitPerspective.Status.Unstaged.Label=Sem Estágio
+GitCommitPerspective.Status.Staged.Label=Estágio ({0})
+GitCommitPerspective.Status.Unstaged.Label=Sem Estágio ({0})
GitCommitPerspective.Toolbar.AddToGit.Tooltip=Adicionar
GitCommitPerspective.Toolbar.Delete.Tooltip=Apagar...
GitCommitPerspective.Toolbar.Refresh.Tooltip=Atualizar
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 a4ca52ce2b..eed022cf71 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
@@ -49,6 +49,7 @@ 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;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
@@ -738,6 +739,70 @@ public class UIGitTest extends RepositoryTestCase {
.orElseThrow(() -> new AssertionError("File '" + name + "' not
found"));
}
+ @Test
+ public void testIgnoreRulesMatchedWithoutRegardToCase() throws Exception {
+ initialCommit();
+
+ // A rule in lower case, folders on disk in another case: git catches
these when it is told the
+ // file system doesn't care about case, JGit doesn't
+ writeTrashFile(".gitignore", "output/\n*.LOG\n");
+ writeTrashFile("Output/generated.txt", "generated");
+ writeTrashFile("Deep/OUTPUT/generated.txt", "generated");
+ writeTrashFile("run.Log", "log");
+ writeTrashFile("keep.txt", "keep");
+
+ // Case sensitive, the way git behaves on Linux: JGit is right, nothing is
filtered
+ List<String> unstaged = getUnstagedFileNames();
+ assertTrue(unstaged.contains("Output/generated.txt"));
+ assertTrue(unstaged.contains("run.Log"));
+
+ db.getConfig()
+ .setBoolean(
+ ConfigConstants.CONFIG_CORE_SECTION,
+ null,
+ CaseInsensitiveIgnores.CONFIG_KEY_IGNORECASE,
+ true);
+ db.getConfig().save();
+
+ unstaged = getUnstagedFileNames();
+ assertTrue(unstaged.contains("keep.txt"));
+ assertFalse(unstaged.contains("Output/generated.txt"));
+ assertFalse(unstaged.contains("Deep/OUTPUT/generated.txt"));
+ assertFalse(unstaged.contains("run.Log"));
+
+ // What is kept out of the unstaged files is reported as ignored instead
+ Set<String> ignored = uiGit.getIgnored(null);
+ assertTrue(ignored.contains("Output/generated.txt"));
+ assertTrue(ignored.contains("run.Log"));
+ assertFalse(ignored.contains("keep.txt"));
+ }
+
+ @Test
+ public void testIgnoreRuleCanBeNegatedWithoutRegardToCase() throws Exception
{
+ initialCommit();
+
+ writeTrashFile(".gitignore", "output/\n!Output/keep.txt\n");
+ writeTrashFile("output/keep.txt", "keep");
+ writeTrashFile("output/generated.txt", "generated");
+
+ db.getConfig()
+ .setBoolean(
+ ConfigConstants.CONFIG_CORE_SECTION,
+ null,
+ CaseInsensitiveIgnores.CONFIG_KEY_IGNORECASE,
+ true);
+ db.getConfig().save();
+
+ // git never descends into an ignored folder, so the negated file stays
ignored as well
+ List<String> unstaged = getUnstagedFileNames();
+ assertFalse(unstaged.contains("output/keep.txt"));
+ assertFalse(unstaged.contains("output/generated.txt"));
+ }
+
+ private List<String> getUnstagedFileNames() {
+ return uiGit.getUnstagedFiles().stream().map(UIFile::getName).toList();
+ }
+
@Test
public void testCreateBranchFromTag() throws Exception {
RevCommit tagged = initialCommit();