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 d820bdd331 issue #8377 - File Explorer Perspective: pipeline 
disappears from tree immediately after Git Commit (#8400)
d820bdd331 is described below

commit d820bdd3314686ea6521bb2b77f635601167dd90
Author: Matt Casters <[email protected]>
AuthorDate: Wed Sep 16 10:42:40 2026 +0200

    issue #8377 - File Explorer Perspective: pipeline disappears from tree 
immediately after Git Commit (#8400)
---
 .../main/java/org/apache/hop/git/GitGuiPlugin.java | 30 +++++--
 .../apache/hop/git/GitGuiPluginCommitFileTest.java | 67 +++++++++++++++
 .../perspective/explorer/ExplorerPerspective.java  | 95 +++++++++++++++-------
 .../apache/hop/ui/core/widget/TreeMemoryTest.java  | 81 ++++++++++++++++++
 4 files changed, 239 insertions(+), 34 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 3e6853c468..54e79a9eb3 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
@@ -18,8 +18,6 @@
 
 package org.apache.hop.git;
 
-import static org.apache.hop.core.vfs.HopVfs.fileExists;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -243,6 +241,7 @@ public class GitGuiPlugin
         return;
       }
       List<String> changedFilesToCommit = git.getRevertPathFiles(relativePath);
+      List<String> committedFiles = new ArrayList<>();
       if (changedFilesToCommit.isEmpty()) {
         MessageBox box =
             new MessageBox(HopGui.getInstance().getShell(), SWT.OK | 
SWT.ICON_INFORMATION);
@@ -280,13 +279,14 @@ public class GitGuiPlugin
             // Now stage/add the selected files and commit...
             //
             int[] selectedNrs = selectionDialog.getSelectionIndeces();
-            List<String> committedFiles = new ArrayList<>();
+            FileObject rootObj = HopVfs.getFileObject(git.getDirectory());
             for (int selectedNr : selectedNrs) {
               // If the file is gone, git.rm(), otherwise add()
               //
               String file = files[selectedNr];
               committedFiles.add(file);
-              if (fileExists(file)) {
+              FileObject fileObj = rootObj.resolveFile(file);
+              if (fileObj.exists()) {
                 git.add(file);
               } else {
                 git.rm(file);
@@ -317,9 +317,27 @@ public class GitGuiPlugin
         }
       }
 
-      // Refresh the tree, change colors...
+      // Refresh the tree, change colors and re-reveal committed files...
       //
-      ExplorerPerspective.getInstance().refresh();
+      ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+      if (explorerPerspective != null) {
+        explorerPerspective.refresh();
+        if (git != null && !committedFiles.isEmpty()) {
+          try {
+            FileObject rootObj = HopVfs.getFileObject(git.getDirectory());
+            for (String file : committedFiles) {
+              FileObject fileObj = rootObj.resolveFile(file);
+              if (fileObj.exists()) {
+                explorerPerspective.selectInTree(HopVfs.getFilename(fileObj), 
false);
+              }
+            }
+          } catch (Exception e) {
+            HopGui.getInstance()
+                .getLog()
+                .logDebug("Unable to reselect committed files in explorer 
tree: " + e.getMessage());
+          }
+        }
+      }
       enableButtons();
     } catch (Exception e) {
       new ErrorDialog(
diff --git 
a/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginCommitFileTest.java
 
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginCommitFileTest.java
new file mode 100644
index 0000000000..971de452af
--- /dev/null
+++ 
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginCommitFileTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.vfs.HopVfs;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class GitGuiPluginCommitFileTest {
+
+  @BeforeAll
+  static void setUpBeforeClass() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @Test
+  void testRepoRelativeFileResolution(@TempDir Path tempDir) throws Exception {
+    FileObject gitDir =
+        
HopVfs.getFileObject(tempDir.resolve("my-repo").toAbsolutePath().toString());
+    gitDir.createFolder();
+
+    FileObject subfolder = gitDir.resolveFile("subfolder");
+    subfolder.createFolder();
+
+    FileObject pipelineFile = subfolder.resolveFile("test-pipeline.hpl");
+    try (OutputStream out = HopVfs.getOutputStream(pipelineFile, false)) {
+      out.write("<pipeline/>".getBytes(StandardCharsets.UTF_8));
+    }
+
+    // Relative path from git status (e.g. subfolder/test-pipeline.hpl)
+    String relativePath = "subfolder/test-pipeline.hpl";
+    FileObject resolved = gitDir.resolveFile(relativePath);
+
+    assertTrue(resolved.exists());
+    assertEquals(HopVfs.getFilename(pipelineFile), 
HopVfs.getFilename(resolved));
+
+    // Non-existent relative file
+    String nonExistent = "subfolder/does-not-exist.hpl";
+    FileObject resolvedNonExistent = gitDir.resolveFile(nonExistent);
+    assertFalse(resolvedNonExistent.exists());
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 985790723e..21a51c5eb7 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -2743,11 +2743,11 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
   }
 
   /** Select the corresponding file in the left-hand tree */
-  private void selectInTree(String filename) {
+  public void selectInTree(String filename) {
     selectInTree(filename, true);
   }
 
-  private void selectInTree(String filename, boolean automatic) {
+  public void selectInTree(String filename, boolean automatic) {
     if (automatic) {
       Boolean activeFileSelection =
           
ExplorerPerspectiveConfigSingleton.getConfig().getActiveFileSelection();
@@ -4277,39 +4277,66 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
         listener.beforeRefresh();
       }
 
+      // Preserve currently expanded items and selection before destroying 
tree items
+      List<String> selectedPaths = new ArrayList<>();
+      if (tree != null && !tree.isDisposed()) {
+        for (TreeItem selected : tree.getSelection()) {
+          TreeItemFolder tif = (TreeItemFolder) selected.getData();
+          if (tif != null && tif.path != null) {
+            selectedPaths.add(tif.path);
+          }
+        }
+        for (TreeItem item : tree.getItems()) {
+          rememberTreeExpandedState(item);
+        }
+      }
+
       tree.setRedraw(false);
-      tree.removeAll();
+      try {
+        tree.removeAll();
 
-      // Add the root element...
-      //
-      TreeItem rootItem = new TreeItem(tree, SWT.NONE);
-      rootItem.setText(Const.NVL(rootName, ""));
-      IHopFileType fileType = getFileType(rootFolder, true);
-      setItemImage(rootItem, fileType);
-      callPaintListeners(tree, rootItem, rootFolder, rootName);
-      setTreeItemData(rootItem, rootFolder, rootName, fileType, 0, true, true);
-
-      // Paint the top level folder only
-      //
-      refreshFolder(rootItem, rootFolder, 0);
+        // Add the root element...
+        //
+        TreeItem rootItem = new TreeItem(tree, SWT.NONE);
+        rootItem.setText(Const.NVL(rootName, ""));
+        IHopFileType fileType = getFileType(rootFolder, true);
+        setItemImage(rootItem, fileType);
+        callPaintListeners(tree, rootItem, rootFolder, rootName);
+        setTreeItemData(rootItem, rootFolder, rootName, fileType, 0, true, 
true);
 
-      // Always expand root item when filtering
-      if (!Utils.isEmpty(filterText)) {
-        rootItem.setExpanded(true);
-        TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem, 
true);
-      } else {
-        TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem, 
true);
+        // Paint the top level folder only
+        //
+        refreshFolder(rootItem, rootFolder, 0);
 
-        // When not filtering, use tree memory (but don't call it here as it 
will be called later)
-        // The TreeMemory will be applied either by restoreTreeState() or 
setExpandedFromMemory()
-        if (treeStateBeforeFilter == null) {
-          // Only restore from memory if we're not about to restore from saved 
state
+        // Always expand root item when filtering
+        if (!Utils.isEmpty(filterText)) {
           rootItem.setExpanded(true);
-          restoreTreeItemExpandedFromMemory(rootItem);
+          TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem, 
true);
+        } else {
+          TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem, 
true);
+
+          // When not filtering, use tree memory (but don't call it here as it 
will be called later)
+          // The TreeMemory will be applied either by restoreTreeState() or 
setExpandedFromMemory()
+          if (treeStateBeforeFilter == null) {
+            // Only restore from memory if we're not about to restore from 
saved state
+            rootItem.setExpanded(true);
+            restoreTreeItemExpandedFromMemory(rootItem);
+          }
         }
-      }
 
-      tree.setRedraw(true);
+        // Restore selection of previously selected items
+        for (String selectedPath : selectedPaths) {
+          selectInTree(selectedPath, false);
+        }
+        if (selectedPaths.isEmpty()) {
+          IHopFileTypeHandler activeHandler = getActiveFileTypeHandler();
+          if (activeHandler != null && 
!Utils.isEmpty(activeHandler.getFilename())) {
+            selectInTree(activeHandler.getFilename(), true);
+          }
+        }
+      } finally {
+        tree.setRedraw(true);
+      }
     } catch (Exception e) {
       new ErrorDialog(
           getShell(),
@@ -4320,6 +4347,18 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     updateSelection();
   }
 
+  private void rememberTreeExpandedState(TreeItem item) {
+    if (item == null || item.isDisposed()) {
+      return;
+    }
+    if (item.getExpanded()) {
+      TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item, true);
+    }
+    for (TreeItem child : item.getItems()) {
+      rememberTreeExpandedState(child);
+    }
+  }
+
   @GuiToolbarElement(
       root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
       id = TOOLBAR_ITEM_SHOW_HIDDEN,
diff --git a/ui/src/test/java/org/apache/hop/ui/core/widget/TreeMemoryTest.java 
b/ui/src/test/java/org/apache/hop/ui/core/widget/TreeMemoryTest.java
new file mode 100644
index 0000000000..8637ca796a
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/widget/TreeMemoryTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ui.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TreeMemoryTest {
+
+  private static final String TREE_NAME = "test-tree";
+
+  @BeforeEach
+  void setUp() {
+    TreeMemory.getInstance().clearTree(TREE_NAME);
+  }
+
+  @Test
+  void testStoreAndCheckExpanded() {
+    TreeMemory memory = TreeMemory.getInstance();
+    assertNotNull(memory);
+
+    String[] rootPath = new String[] {"project"};
+    String[] subPath = new String[] {"project", "subfolder"};
+    String[] otherPath = new String[] {"project", "other"};
+
+    assertFalse(memory.isExpanded(TREE_NAME, rootPath));
+    assertFalse(memory.isExpanded(TREE_NAME, subPath));
+
+    memory.storeExpanded(TREE_NAME, rootPath, true);
+    memory.storeExpanded(TREE_NAME, subPath, true);
+
+    assertTrue(memory.isExpanded(TREE_NAME, rootPath));
+    assertTrue(memory.isExpanded(TREE_NAME, subPath));
+    assertFalse(memory.isExpanded(TREE_NAME, otherPath));
+
+    // Collapse subfolder
+    memory.storeExpanded(TREE_NAME, subPath, false);
+    assertTrue(memory.isExpanded(TREE_NAME, rootPath));
+    assertFalse(memory.isExpanded(TREE_NAME, subPath));
+  }
+
+  @Test
+  void testClearTree() {
+    TreeMemory memory = TreeMemory.getInstance();
+
+    String[] path1 = new String[] {"root", "folder1"};
+    String[] path2 = new String[] {"root", "folder2"};
+
+    memory.storeExpanded(TREE_NAME, path1, true);
+    memory.storeExpanded(TREE_NAME, path2, true);
+    memory.storeExpanded("another-tree", path1, true);
+
+    memory.clearTree(TREE_NAME);
+
+    assertFalse(memory.isExpanded(TREE_NAME, path1));
+    assertFalse(memory.isExpanded(TREE_NAME, path2));
+    assertTrue(memory.isExpanded("another-tree", path1));
+
+    memory.clearTree("another-tree");
+    assertFalse(memory.isExpanded("another-tree", path1));
+  }
+}

Reply via email to