This is an automated email from the ASF dual-hosted git repository.

mattcasters 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 356212bb51 unstage files in the git commit perspective, fixes #7894. 
(#7900)
356212bb51 is described below

commit 356212bb51ae138efe93415c8b21a355ecc4a92b
Author: Bart Maertens <[email protected]>
AuthorDate: Tue Aug 11 20:26:41 2026 +0200

    unstage files in the git commit perspective, fixes #7894. (#7900)
---
 .../ROOT/assets/images/icons/git-restore.svg       |   3 +
 .../ROOT/assets/images/icons/git-unstage.svg       |  33 +++++
 .../modules/ROOT/pages/hop-gui/hop-gui-git.adoc    |  24 ++++
 .../org/apache/hop/git/GitCommitPerspective.java   | 134 ++++++++++++++++-----
 .../misc/git/src/main/resources/git-unstage.svg    |  33 +++++
 .../hop/git/messages/messages_en_US.properties     |   8 +-
 .../java/org/apache/hop/git/model/UIGitTest.java   |  54 +++++++++
 7 files changed, 257 insertions(+), 32 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-restore.svg 
b/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-restore.svg
new file mode 100644
index 0000000000..2824a03811
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-restore.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24" viewBox="0 0 24 
24">
+    <path fill="#0E3A5A" d="M20 13.5C20 17.09 17.09 20 13.5 20H6V18H13.5C16 18 
18 16 18 13.5S16 9 13.5 9H7.83L10.91 12.09L9.5 13.5L4 8L9.5 2.5L10.92 3.91L7.83 
7H13.5C17.09 7 20 9.91 20 13.5Z" />
+</svg>
\ No newline at end of file
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-unstage.svg 
b/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-unstage.svg
new file mode 100644
index 0000000000..ffc918179d
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/icons/git-unstage.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  - 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.
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; x="0px" y="0px"
+        width="97px" height="97px" viewBox="0 0 97 97" enable-background="new 
0 0 97 97" xml:space="preserve">
+<g transform="matrix(0.84298576,0,0,0.84298576,0.40113214,0.40160318)">
+       <path fill="#F05133" 
d="M92.71,44.408L52.591,4.291c-2.31-2.311-6.057-2.311-8.369,0l-8.33,8.332L46.459,23.19
+               
c2.456-0.829,5.272-0.273,7.229,1.686c1.969,1.97,2.521,4.81,1.67,7.275l10.187,10.185c2.465-0.85,5.306-0.3,7.275,1.671
+               
c2.749,2.75,2.749,7.206,0,9.958c-2.752,2.751-7.208,2.751-9.962,0c-2.068-2.07-2.58-5.11-1.53-7.658l-9.501-9.499v24.998
+               
c0.67,0.331,1.303,0.774,1.861,1.332c2.75,2.749,2.75,7.206,0,9.958c-2.75,2.749-7.208,2.749-9.957,0
+               
c-2.75-2.754-2.75-7.21,0-9.958c0.68-0.68,1.467-1.193,2.307-1.538V36.369c-0.84-0.344-1.625-0.853-2.307-1.537
+               
c-2.083-2.082-2.584-5.14-1.516-7.698L31.798,16.715L4.288,44.222c-2.311,2.313-2.311,6.06,0,8.371l40.121,40.118
+               
c2.31,2.311,6.056,2.311,8.369,0L92.71,52.779C95.02,50.468,95.02,46.719,92.71,44.408z"/>
+</g>
+<g transform="matrix(1.1836353,0,0,1.1836353,57.486934,57.295456)">
+       <polygon fill="#0E3A5A" points="-0.001,12.4 32,12.4 32,19.6 -0.001,19.6 
        "/>
+</g>
+</svg>
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 1c23bdf3c8..f7734e8255 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
@@ -97,6 +97,30 @@ A transform or action that was renamed is marked as changed, 
rather than as one
 The hops around it are left alone.
 This is recognised as long as the rename is all that happened to it, a move 
aside: renaming and editing in the same commit still shows up as a removal plus 
an addition, because there is nothing left to recognise it by.
 
+== 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 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.
+
+[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-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
+|===
+
+WARNING: Unstage and Restore both act on staged files, but they do very 
different things.
+Unstage only takes a file out of the next commit and keeps your work, Restore 
throws your changes away.
+That is why Restore asks for confirmation and Unstage does not: nothing is 
lost by unstaging, you can stage the file again with 'Add'.
+
+The same options are available in the right-click menu of the file list.
+
 == Git Configuration options
 
 There are a few global configuration options available for the git GUI plugin.
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 9a75636b3f..928dfd5530 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
@@ -19,8 +19,10 @@ package org.apache.hop.git;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import lombok.Getter;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.Props;
@@ -96,6 +98,8 @@ public class GitCommitPerspective implements IHopPerspective {
   public static final String CONTEXT_MENU_ADD = 
"GitCommitPerspective-ContextMenu-10100-AddToGit";
   public static final String CONTEXT_MENU_ADD_TO_GIT_IGNORE =
       "GitCommitPerspective-ContextMenu-10110-AddToGitIgnore";
+  public static final String CONTEXT_MENU_UNSTAGE =
+      "GitCommitPerspective-ContextMenu-10120-Unstage";
   public static final String CONTEXT_MENU_SHOW_TEXT_DIFF =
       "GitCommitPerspective-ContextMenu-10200-ShowTextDiff";
   public static final String CONTEXT_MENU_SHOW_GRAPH_DIFF =
@@ -107,6 +111,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
   public static final String GUI_PLUGIN_TOOLBAR_PARENT_ID = 
"GitCommitPerspective-Toolbar";
   public static final String TOOLBAR_ITEM_REFRESH = 
"GitCommitPerspective-Toolbar-10100-Refresh";
   public static final String TOOLBAR_ITEM_ADD = 
"GitCommitPerspective-Toolbar-10200-Add";
+  public static final String TOOLBAR_ITEM_UNSTAGE = 
"GitCommitPerspective-Toolbar-10250-Unstage";
   public static final String TOOLBAR_ITEM_RESTORE = 
"GitCommitPerspective-Toolbar-10300-Restore";
   public static final String TOOLBAR_ITEM_DELETE = 
"GitCommitPerspective-Toolbar-10400-Delete";
 
@@ -254,6 +259,8 @@ public class GitCommitPerspective implements 
IHopPerspective {
           List<UIFile> selectedFiles = this.getSelectedFiles();
 
           setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD, 
!getSelectedUntrackedFiles().isEmpty());
+          setMenuItemEnabled(
+              menuWidgets, CONTEXT_MENU_UNSTAGE, 
!getSelectedStagedFiles().isEmpty());
           setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD_TO_GIT_IGNORE, 
!selectedFiles.isEmpty());
           setMenuItemEnabled(menuWidgets, CONTEXT_MENU_SHOW_TEXT_DIFF, 
!selectedFiles.isEmpty());
           setMenuItemEnabled(
@@ -328,33 +335,54 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
   @GuiKeyboardShortcut(control = true, key = 'k', global = true)
   @GuiOsxKeyboardShortcut(command = true, key = 'k', global = true)
-  public void selectAllStaged() {
-    checkStagedFiles();
+  public void selectAllChanged() {
+    checkChangedFiles();
 
     wMessage.selectAll();
     wMessage.setFocus();
   }
 
   /**
-   * Check the staged files and uncheck the unstaged ones. Staged files are 
the ones that go into
-   * the next commit, so they are what the commit buttons act on by default.
+   * Check the files git already knows about, staged or not: the commit acts 
on what is checked, so
+   * this offers all the work in progress for the next commit. Untracked files 
are left unchecked:
+   * committing a file for the first time is a deliberate act, git doesn't do 
it for you either.
    */
-  private void checkStagedFiles() {
+  private void checkChangedFiles() {
     if (wTree == null || wTree.isDisposed() || wTree.getItemCount() < 2) {
       return;
     }
 
-    TreeItem stagedRootItem = this.getStagedRootItem();
-    stagedRootItem.setChecked(stagedRootItem.getItemCount() > 0);
-    for (TreeItem item : stagedRootItem.getItems()) {
-      item.setChecked(true);
+    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);
+    }
+  }
+
+  /** A file git doesn't know about yet: added in the working tree, not in the 
index. */
+  private boolean isUntracked(UIFile file) {
+    return !file.isStaged() && file.getChangeType() == 
DiffEntry.ChangeType.ADD;
+  }
+
+  /** Uncheck the given files under the unstaged node, they are no longer part 
of the commit. */
+  private void uncheckUnstagedFiles(Set<String> fileNames) {
+    if (wTree == null || wTree.isDisposed() || wTree.getItemCount() < 2 || 
fileNames.isEmpty()) {
+      return;
     }
 
     TreeItem unstagedRootItem = this.getUnstagedRootItem();
-    unstagedRootItem.setChecked(false);
+    boolean allChecked = true;
     for (TreeItem item : unstagedRootItem.getItems()) {
-      item.setChecked(false);
+      if (item.getData() instanceof UIFile file && 
fileNames.contains(file.getName())) {
+        item.setChecked(false);
+      }
+      allChecked = allChecked && item.getChecked();
     }
+    unstagedRootItem.setChecked(allChecked && unstagedRootItem.getItemCount() 
> 0);
   }
 
   protected void retrieveState() {
@@ -462,6 +490,58 @@ public class GitCommitPerspective implements 
IHopPerspective {
     }
   }
 
+  /**
+   * Unstage the selected staged files: the equivalent of git restore 
--staged. The changes are kept
+   * in the working tree, only the index is updated. Use {@link 
#restoreFiles()} to throw the
+   * changes away.
+   */
+  @GuiMenuElement(
+      root = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      parentId = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      id = CONTEXT_MENU_UNSTAGE,
+      label = "i18n::GitCommitPerspective.Menu.Unstage.Text",
+      image = "git-unstage.svg")
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ITEM_UNSTAGE,
+      toolTip = "i18n::GitCommitPerspective.Toolbar.Unstage.Tooltip",
+      image = "git-unstage.svg")
+  @GuiKeyboardShortcut(control = true, alt = true, key = 'U')
+  @GuiOsxKeyboardShortcut(control = true, alt = true, key = 'U')
+  public void unstageFiles() {
+    try {
+      List<UIFile> files = getSelectedStagedFiles();
+      if (files.isEmpty()) {
+        return;
+      }
+
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      Set<String> unstagedFileNames = new HashSet<>();
+      for (UIFile file : files) {
+        git.resetPath(file.getName());
+        unstagedFileNames.add(file.getName());
+      }
+
+      GitGuiPlugin.getInstance().beforeRefresh();
+      refresh();
+
+      // Taking a file out of the commit is the point of unstaging it, so 
leave these unchecked
+      // even though refresh() offers every changed file by default.
+      //
+      uncheckUnstagedFiles(unstagedFileNames);
+      updateGui();
+
+      // Refresh the tree, change colors...
+      ExplorerPerspective.getInstance().refresh();
+    } catch (Exception e) {
+      new ErrorDialog(
+          getShell(),
+          BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.UnstageError.Header"),
+          BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.UnstageError.Message"),
+          e);
+    }
+  }
+
   @GuiMenuElement(
       root = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
       parentId = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
@@ -556,6 +636,8 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiToolbarElement(
       root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
       id = TOOLBAR_ITEM_RESTORE,
+      // Keep restore, which throws changes away, out of the staging group
+      separator = true,
       toolTip = "i18n::GitCommitPerspective.Toolbar.Restore.Tooltip",
       image = "git-restore.svg")
   @GuiKeyboardShortcut(alt = true, control = true, key = 'Z')
@@ -900,6 +982,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
     wMessage.setEnabled(gitEnabled);
 
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_ADD, 
!getSelectedUntrackedFiles().isEmpty());
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_UNSTAGE, 
!getSelectedStagedFiles().isEmpty());
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_RESTORE, 
!getSelectedStagedFiles().isEmpty());
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_DELETE, 
!getSelectedFiles().isEmpty());
 
@@ -941,30 +1024,21 @@ public class GitCommitPerspective implements 
IHopPerspective {
         GitResource resource = GitResource.getInstance();
         for (String fileName : filesToCommit.keySet()) {
           UIFile file = filesToCommit.get(fileName);
-          TreeItem item = null;
+
+          // 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);
           switch (file.getChangeType()) {
-            case ADD, COPY, RENAME:
-              {
-                item = new TreeItem(file.isStaged() ? stagedItem : 
untrackedItem, SWT.NONE);
+            case ADD, COPY, RENAME ->
                 item.setForeground(
                     file.isStaged() ? resource.getStagedAddColor() : 
resource.getUnstagedColor());
-                break;
-              }
-            case MODIFY:
-              {
-                item = new TreeItem(stagedItem, SWT.NONE);
+            case MODIFY ->
                 item.setForeground(
                     file.isStaged()
                         ? resource.getStagedModifyColor()
                         : resource.getUnstagedColor());
-                break;
-              }
-            case DELETE:
-              {
-                item = new TreeItem(stagedItem, SWT.NONE);
-                item.setForeground(resource.getIgnoredColor());
-                break;
-              }
+            case DELETE -> item.setForeground(resource.getIgnoredColor());
           }
 
           item.setText(file.getName());
@@ -975,9 +1049,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
         stagedItem.setExpanded(true);
         untrackedItem.setExpanded(true);
 
-        // Start from the staged files, the way a "git commit" would
+        // Offer all the work in progress for the next commit
         //
-        checkStagedFiles();
+        checkChangedFiles();
       }
       wTree.setRedraw(true);
 
diff --git a/plugins/misc/git/src/main/resources/git-unstage.svg 
b/plugins/misc/git/src/main/resources/git-unstage.svg
new file mode 100644
index 0000000000..ffc918179d
--- /dev/null
+++ b/plugins/misc/git/src/main/resources/git-unstage.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  - 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.
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; x="0px" y="0px"
+        width="97px" height="97px" viewBox="0 0 97 97" enable-background="new 
0 0 97 97" xml:space="preserve">
+<g transform="matrix(0.84298576,0,0,0.84298576,0.40113214,0.40160318)">
+       <path fill="#F05133" 
d="M92.71,44.408L52.591,4.291c-2.31-2.311-6.057-2.311-8.369,0l-8.33,8.332L46.459,23.19
+               
c2.456-0.829,5.272-0.273,7.229,1.686c1.969,1.97,2.521,4.81,1.67,7.275l10.187,10.185c2.465-0.85,5.306-0.3,7.275,1.671
+               
c2.749,2.75,2.749,7.206,0,9.958c-2.752,2.751-7.208,2.751-9.962,0c-2.068-2.07-2.58-5.11-1.53-7.658l-9.501-9.499v24.998
+               
c0.67,0.331,1.303,0.774,1.861,1.332c2.75,2.749,2.75,7.206,0,9.958c-2.75,2.749-7.208,2.749-9.957,0
+               
c-2.75-2.754-2.75-7.21,0-9.958c0.68-0.68,1.467-1.193,2.307-1.538V36.369c-0.84-0.344-1.625-0.853-2.307-1.537
+               
c-2.083-2.082-2.584-5.14-1.516-7.698L31.798,16.715L4.288,44.222c-2.311,2.313-2.311,6.06,0,8.371l40.121,40.118
+               
c2.31,2.311,6.056,2.311,8.369,0L92.71,52.779C95.02,50.468,95.02,46.719,92.71,44.408z"/>
+</g>
+<g transform="matrix(1.1836353,0,0,1.1836353,57.486934,57.295456)">
+       <polygon fill="#0E3A5A" points="-0.001,12.4 32,12.4 32,19.6 -0.001,19.6 
        "/>
+</g>
+</svg>
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 ddebb027d4..e957e2b51f 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
@@ -80,6 +80,8 @@ GitGuiPlugin.Dialog.RevertCommitConfirmation.Message=Are you 
sure you want to re
 GitGuiPlugin.Dialog.RevertCommitRepositoryIsNotClean.Message=Please clean your 
repository working tree before reverting commits.
 GitGuiPlugin.Dialog.RevertFiles.Header=Select files to revert
 GitGuiPlugin.Dialog.RevertFiles.Message=Please select the files to revert
+GitGuiPlugin.Dialog.UnstageError.Header=GIT Unstage Error
+GitGuiPlugin.Dialog.UnstageError.Message=There was an error unstaging the 
selected files
 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
@@ -126,6 +128,7 @@ GitCommitPerspective.Button.Restore.Label=Restore
 GitCommitPerspective.Menu.AddToGit.Text=Add
 GitCommitPerspective.Menu.AddToGitIgnore.Text=Add to .gitignore
 GitCommitPerspective.Menu.Restore.Text=Restore...
+GitCommitPerspective.Menu.Unstage.Text=Unstage
 GitCommitPerspective.Menu.ShowTextDiff.Text=Show text diff
 GitCommitPerspective.Menu.ShowGraphDiff.Text=Show visual diff
 GitCommitPerspective.Menu.Delete.Text=Delete...
@@ -135,10 +138,11 @@ 
GitCommitPerspective.Status.CommittedAndPushed.Message=Committed and pushed {0}
 GitCommitPerspective.Status.CommitId.Label=as {0}
 GitCommitPerspective.Status.Staged.Label=Staged
 GitCommitPerspective.Status.Unstaged.Label=Unstaged
-GitCommitPerspective.Toolbar.AddToGit.Tooltip=Add
+GitCommitPerspective.Toolbar.AddToGit.Tooltip=Stage the selected files
 GitCommitPerspective.Toolbar.Delete.Tooltip=Delete...
 GitCommitPerspective.Toolbar.Refresh.Tooltip=Refresh
-GitCommitPerspective.Toolbar.Restore.Tooltip=Restore file to the last 
committed version (HEAD)...
+GitCommitPerspective.Toolbar.Restore.Tooltip=Discard changes, restore the last 
committed version (HEAD)...
+GitCommitPerspective.Toolbar.Unstage.Tooltip=Unstage the selected files, the 
changes are kept
 
 GitPerspective.Description=Git perspective (branches, tags, commit history)
 GitPerspective.Name=Git
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 670baccdba..b500bb1e57 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
@@ -652,6 +652,60 @@ public class UIGitTest extends RepositoryTestCase {
     assertFalse(tags.contains("test"));
   }
 
+  @Test
+  public void testUnstageFilesWithResetPath() throws Exception {
+    initialCommit();
+
+    // Stage a change to a tracked file and a brand new file
+    writeTrashFile("Test.txt", "Changed");
+    uiGit.add("Test.txt");
+    writeTrashFile("New.txt", "New file");
+    uiGit.add("New.txt");
+    assertEquals(2, uiGit.getStagedFiles().size());
+
+    for (UIFile file : uiGit.getStagedFiles()) {
+      uiGit.resetPath(file.getName());
+    }
+
+    // Nothing staged anymore, but both changes are still there
+    assertTrue(uiGit.getStagedFiles().isEmpty());
+    assertEquals(2, uiGit.getUnstagedFiles().size());
+    assertEquals("Changed", read(new File(db.getWorkTree(), "Test.txt")));
+  }
+
+  @Test
+  public void testUntrackedAndModifiedFileFlags() throws Exception {
+    initialCommit();
+
+    // A file git doesn't know about yet and a change to a file it does know 
about
+    writeTrashFile("New.txt", "New file");
+    writeTrashFile("Test.txt", "Changed");
+
+    List<UIFile> unstagedFiles = uiGit.getUnstagedFiles();
+    UIFile untracked = findFile(unstagedFiles, "New.txt");
+    UIFile modified = findFile(unstagedFiles, "Test.txt");
+
+    // The commit perspective tells both apart by change type to decide what 
it offers for the
+    // next commit: an unstaged ADD is a file git doesn't track yet, which is 
never checked for you
+    assertEquals(ChangeType.ADD, untracked.getChangeType());
+    assertFalse(untracked.isStaged());
+    assertEquals(ChangeType.MODIFY, modified.getChangeType());
+    assertFalse(modified.isStaged());
+
+    // Once added, the same new file is staged
+    uiGit.add("New.txt");
+    UIFile staged = findFile(uiGit.getStagedFiles(), "New.txt");
+    assertEquals(ChangeType.ADD, staged.getChangeType());
+    assertTrue(staged.isStaged());
+  }
+
+  private UIFile findFile(List<UIFile> files, String name) {
+    return files.stream()
+        .filter(file -> file.getName().equals(name))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("File '" + name + "' not 
found"));
+  }
+
   @Test
   public void testCloneShouldFail() throws Exception {
     // WhenDirAlreadyExists

Reply via email to