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));
+ }
+}