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 f43bd97186 Issue #8251: Multi-user session isolation and hardening for 
Git plugin (#8365)
f43bd97186 is described below

commit f43bd971869e2cb484771f3c86031c7b6c81da4f
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 15 12:43:37 2026 +0200

    Issue #8251: Multi-user session isolation and hardening for Git plugin 
(#8365)
    
    - Resolve cross-session bleeding in multi-user Hop Web by removing 
process-wide static singletons from GitPerspective and GitCommitPerspective.
    
    - Guard all public perspective methods with isInitialized() and display 
asyncExec checks to prevent SWT invalid thread access and NPEs when 
perspectives are disabled or uninitialized.
    
    - Safely fall back to ExplorerPerspective.refresh() and web commit dialogs 
when GitPerspective or GitCommitPerspective are disabled in the active session.
    
    - Fix resource and listener leaks: auto-close PlotWalk, cancel automatic 
fetch timers, close git repositories, and unregister from HopGuiKeyHandler on 
shell disposal.
    
    - Replace java.io.File usage with Apache Commons VFS (HopVfs).
    
    - Synchronize GitConfigSingleton to ensure thread-safe config access.
    
    - Add comprehensive session isolation and uninitialized safety tests in 
GitGuiPluginSessionIsolationTest.
---
 .../org/apache/hop/git/GitCommitPerspective.java   | 252 ++++++---
 .../main/java/org/apache/hop/git/GitGuiPlugin.java | 118 +++--
 .../java/org/apache/hop/git/GitPerspective.java    | 579 ++++++++++++++-------
 .../apache/hop/git/config/GitConfigSingleton.java  |  17 +-
 .../git/info/GitInfoExplorerFileTypeHandler.java   |  14 +-
 .../hop/git/GitGuiPluginSessionIsolationTest.java  |  87 ++++
 6 files changed, 763 insertions(+), 304 deletions(-)

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 f20044f05c..6202c45141 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
@@ -17,7 +17,6 @@
 
 package org.apache.hop.git;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -118,7 +117,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
   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";
-  private static GitCommitPerspective instance;
+  private static GitCommitPerspective testFallback;
 
   private HopGui hopGui;
   private SashForm wSashForm;
@@ -136,19 +135,25 @@ public class GitCommitPerspective implements 
IHopPerspective {
   private GuiMenuWidgets menuWidgets;
 
   public GitCommitPerspective() {
-    instance = this;
+    if (HopGui.peekInstance() == null) {
+      testFallback = this;
+    }
   }
 
   public static GitCommitPerspective getInstance() {
-    try {
-      GitCommitPerspective fromGui = 
HopGui.findSessionPerspective(GitCommitPerspective.class);
-      if (fromGui != null) {
-        return fromGui;
-      }
-    } catch (Throwable e) {
-      // No HopGuiImpl in unit tests
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null) {
+      return HopGui.findSessionPerspective(GitCommitPerspective.class);
     }
-    return instance;
+    return testFallback;
+  }
+
+  public static void setTestFallback(GitCommitPerspective fallback) {
+    testFallback = fallback;
+  }
+
+  public boolean isInitialized() {
+    return hopGui != null && wTree != null && !wTree.isDisposed();
   }
 
   @Override
@@ -160,7 +165,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'o', global = 
true)
   @Override
   public void activate() {
-    if (hopGui == null) {
+    if (!isInitialized()) {
       // The perspective is switched off in disabledGuiElements.xml: it was 
never initialized, so
       // there is nothing to switch to. The git toolbar and menu items that 
lead here live on the
       // explorer perspective and are still there.
@@ -184,6 +189,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
   @Override
   public void perspectiveActivated() {
+    if (!isInitialized()) {
+      return;
+    }
     // TODO: avoid refresh when perspective is activated, detect change in the 
config, metadata,
     // file explorer
     refresh();
@@ -191,7 +199,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
   @Override
   public boolean isActive() {
-    return hopGui != null && hopGui.isActivePerspective(this);
+    return isInitialized() && hopGui.isActivePerspective(this);
   }
 
   @Override
@@ -211,29 +219,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
     retrieveState();
     refresh();
 
-    // Refresh the state when a file changes
-    // TODO: For now we refresh when we active the perspective
-    //    hopGui
-    //        .getEventsHandler()
-    //        .addEventListener(
-    //            getClass().getName(),
-    //            e -> refresh(),
-    //            HopGuiEvents.ProjectUpdated.name(),
-    //            HopGuiEvents.PipelineCreated.name(),
-    //            HopGuiEvents.PipelineUpdated.name(),
-    //            HopGuiEvents.PipelineDeleted.name(),
-    //            HopGuiEvents.WorkflowCreated.name(),
-    //            HopGuiEvents.WorkflowUpdated.name(),
-    //            HopGuiEvents.WorkflowDeleted.name(),
-    //            HopGuiEvents.MetadataCreated.name(),
-    //            HopGuiEvents.MetadataChanged.name(),
-    //            HopGuiEvents.MetadataDeleted.name(),
-    //            HopGuiEvents.FileCreated.name(),
-    //            HopGuiEvents.FileChanged.name(),
-    //            HopGuiEvents.FileDeleted.name()
-    //        );
-
     HopGuiKeyHandler.getInstance().addParentObjectToHandle(this);
+    wSashForm.addListener(
+        SWT.Dispose, e -> 
HopGuiKeyHandler.getInstance().removeParentObjectToHandle(this));
   }
 
   protected void createTree(Composite parent) {
@@ -353,10 +341,15 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, key = 'k', global = true)
   @GuiOsxKeyboardShortcut(command = true, key = 'k', global = true)
   public void selectAllChanged() {
+    if (!isInitialized()) {
+      return;
+    }
     checkChangedFiles();
 
-    wMessage.selectAll();
-    wMessage.setFocus();
+    if (wMessage != null && !wMessage.isDisposed()) {
+      wMessage.selectAll();
+      wMessage.setFocus();
+    }
   }
 
   /**
@@ -411,7 +404,11 @@ public class GitCommitPerspective implements 
IHopPerspective {
   }
 
   protected void retrieveState() {
-    if (wMessage == null || wMessage.isDisposed()) {
+    if (!isInitialized() || wMessage == null || wMessage.isDisposed()) {
+      return;
+    }
+    if (wMessage.getDisplay() != org.eclipse.swt.widgets.Display.getCurrent()) 
{
+      wMessage.getDisplay().asyncExec(this::retrieveState);
       return;
     }
     try {
@@ -430,6 +427,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
   }
 
   protected void saveState() {
+    if (!isInitialized() || wMessage == null || wMessage.isDisposed()) {
+      return;
+    }
     try {
       List<String> messages = new ArrayList<>();
       messages.add(wMessage.getText());
@@ -490,6 +490,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, alt = true, key = 'A')
   @GuiOsxKeyboardShortcut(control = true, alt = true, key = 'A')
   public void addFilesToGit() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
       List<UIFile> files = getSelectedFiles();
       if (files.isEmpty()) {
@@ -497,6 +500,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
       }
 
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       for (UIFile file : files) {
         git.add(file.getName());
       }
@@ -534,6 +540,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, alt = true, key = 'U')
   @GuiOsxKeyboardShortcut(control = true, alt = true, key = 'U')
   public void unstageFiles() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
       List<UIFile> files = getSelectedStagedFiles();
       if (files.isEmpty()) {
@@ -541,6 +550,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
       }
 
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       Set<String> unstagedFileNames = new HashSet<>();
       for (UIFile file : files) {
         git.resetPath(file.getName());
@@ -574,12 +586,18 @@ public class GitCommitPerspective implements 
IHopPerspective {
       label = "i18n::GitCommitPerspective.Menu.AddToGitIgnore.Text",
       image = "ignore.svg")
   public void addFilesToGitIgnore() {
+    if (!isInitialized()) {
+      return;
+    }
     List<UIFile> files = getSelectedFiles();
     if (files.isEmpty()) {
       return;
     }
 
     UIGit git = GitGuiPlugin.getInstance().getGit();
+    if (git == null) {
+      return;
+    }
     for (UIFile file : files) {
       git.addPathToIgnore(file.getName());
     }
@@ -607,10 +625,16 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(key = SWT.DEL)
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   public void deleteFiles() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
       List<UIFile> files = getSelectedFiles();
       if (!files.isEmpty()) {
         UIGit git = GitGuiPlugin.getInstance().getGit();
+        if (git == null) {
+          return;
+        }
         ExplorerPerspective perspective = ExplorerPerspective.getInstance();
 
         List<String> filesToClose = new ArrayList<>();
@@ -639,8 +663,10 @@ public class GitCommitPerspective implements 
IHopPerspective {
         refresh();
 
         // Refresh perspectives (file explorer, metadata)
-        // TODO: Find a better way
-        GitPerspective.getInstance().refresh(true);
+        GitPerspective gitPerspective = GitPerspective.getInstance();
+        if (gitPerspective != null && gitPerspective.isInitialized()) {
+          gitPerspective.refresh(true);
+        }
       }
     } catch (Exception e) {
       new ErrorDialog(
@@ -668,6 +694,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(alt = true, control = true, key = 'Z')
   @GuiOsxKeyboardShortcut(alt = true, control = true, key = 'Z')
   public void restoreFiles() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
       // Restore only selected staged file, use delete for untracked files
       List<UIFile> files = this.getSelectedStagedFiles();
@@ -694,6 +723,9 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
         boolean deleteAddedFiles = dialog.getToggleState();
         UIGit git = GitGuiPlugin.getInstance().getGit();
+        if (git == null) {
+          return;
+        }
 
         List<String> filesToClose = new ArrayList<>();
         List<String> filesToReload = new ArrayList<>();
@@ -733,8 +765,10 @@ public class GitCommitPerspective implements 
IHopPerspective {
         refresh();
 
         // Refresh perspectives (file explorer, metadata)
-        // TODO: Find a better way
-        GitPerspective.getInstance().refresh(true);
+        GitPerspective gitPerspective = GitPerspective.getInstance();
+        if (gitPerspective != null && gitPerspective.isInitialized()) {
+          gitPerspective.refresh(true);
+        }
       }
 
     } catch (Exception e) {
@@ -748,15 +782,11 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
   private String getAbsolutePath(String root, String relativePath) {
     try {
-      FileObject fileObj = HopVfs.getFileObject(new File(root, 
relativePath).getAbsolutePath());
-      String path =
-          fileObj.exists()
-              ? HopVfs.getFilename(fileObj)
-              : new File(root, relativePath).getAbsolutePath();
-
-      return path;
+      FileObject rootObj = HopVfs.getFileObject(root);
+      FileObject fileObj = rootObj.resolveFile(relativePath);
+      return HopVfs.getFilename(fileObj);
     } catch (Exception ignored) {
-      return new File(root, relativePath).getAbsolutePath();
+      return root + "/" + relativePath;
     }
   }
 
@@ -770,11 +800,12 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, key = 'D')
   @GuiOsxKeyboardShortcut(control = true, key = 'D')
   public void showTextDiff() {
+    if (!isInitialized() || wTree.getSelectionCount() == 0) {
+      return;
+    }
     try {
       TreeItem treeItem = wTree.getSelection()[0];
-      if (treeItem != null) {
-        UIFile file = (UIFile) treeItem.getData();
-
+      if (treeItem != null && treeItem.getData() instanceof UIFile file) {
         String commitIdNew = VCS.WORKINGTREE;
         String commitIdOld = Constants.HEAD;
 
@@ -782,7 +813,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
       }
     } catch (Exception e) {
       new ErrorDialog(
-          HopGui.getInstance().getShell(),
+          getShell(),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.ShowDiffError.Header"),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.ShowDiffError.Message"),
           e);
@@ -798,20 +829,25 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, shift = true, key = 'D')
   @GuiOsxKeyboardShortcut(control = true, shift = true, key = 'D')
   public void showGraphDiff() {
+    if (!isInitialized() || wTree.getSelectionCount() == 0) {
+      return;
+    }
     try {
       TreeItem treeItem = wTree.getSelection()[0];
-      if (treeItem != null) {
-        UIFile file = (UIFile) treeItem.getData();
-
+      if (treeItem != null && treeItem.getData() instanceof UIFile file) {
         String commitIdNew = VCS.WORKINGTREE;
         String commitIdOld = Constants.HEAD;
         ExplorerPerspective perspective = HopGui.getExplorerPerspective();
 
-        if (perspective.getPipelineFileType().isHandledBy(file.getName(), 
false)) {
+        if (perspective != null
+            && perspective.getPipelineFileType() != null
+            && perspective.getPipelineFileType().isHandledBy(file.getName(), 
false)) {
           // A pipeline
           //
           GitGuiPlugin.getInstance().showPipelineFileDiff(file.getName(), 
commitIdNew, commitIdOld);
-        } else if 
(perspective.getWorkflowFileType().isHandledBy(file.getName(), false)) {
+        } else if (perspective != null
+            && perspective.getWorkflowFileType() != null
+            && perspective.getWorkflowFileType().isHandledBy(file.getName(), 
false)) {
           // A workflow
           //
           GitGuiPlugin.getInstance().showWorkflowFileDiff(file.getName(), 
commitIdNew, commitIdOld);
@@ -819,7 +855,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
       }
     } catch (Exception e) {
       new ErrorDialog(
-          HopGui.getInstance().getShell(),
+          getShell(),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.ShowDiffError.Header"),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.ShowDiffError.Message"),
           e);
@@ -827,7 +863,14 @@ public class GitCommitPerspective implements 
IHopPerspective {
   }
 
   public void commitFiles(boolean push) {
+    if (!isInitialized()) {
+      return;
+    }
     try {
+      UIGit uiGit = GitGuiPlugin.getInstance().getGit();
+      if (uiGit == null) {
+        return;
+      }
 
       // Selected staged or unstaged files
       List<UIFile> filesToCommit = this.getSelectedFiles(wTree.getItems(), new 
ArrayList<>(), true);
@@ -854,7 +897,6 @@ public class GitCommitPerspective implements 
IHopPerspective {
       }
 
       // Standard author by default
-      UIGit uiGit = GitGuiPlugin.getInstance().getGit();
       String authorName = uiGit.getAuthorName(VCS.WORKINGTREE);
       String message = wMessage.getText();
       boolean amend = wAmend.getSelection();
@@ -905,7 +947,10 @@ public class GitCommitPerspective implements 
IHopPerspective {
       refresh();
 
       // Refresh file and git perspectives
-      GitPerspective.getInstance().refresh(true);
+      GitPerspective gitPerspective = GitPerspective.getInstance();
+      if (gitPerspective != null && gitPerspective.isInitialized()) {
+        gitPerspective.refresh(true);
+      }
 
       wAmend.setSelection(false);
 
@@ -915,7 +960,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
           pushed = uiGit.push();
         } catch (Exception e) {
           new ErrorDialog(
-              HopGui.getInstance().getShell(),
+              getShell(),
               BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Header"),
               BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Message"),
               e);
@@ -936,7 +981,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
               getCommitIdLabel(commitId)));
     } catch (Exception e) {
       new ErrorDialog(
-          HopGui.getInstance().getShell(),
+          getShell(),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.CommitError.Header"),
           BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.CommitError.Message"),
           e);
@@ -967,9 +1012,11 @@ public class GitCommitPerspective implements 
IHopPerspective {
    * #updateGui()} so it lives exactly as long as the user's next action.
    */
   private void showStatus(Image image, String message) {
-    wStatus.setImage(image);
-    wStatus.setText(message);
-    wStatus.setVisible(true);
+    if (wStatus != null && !wStatus.isDisposed()) {
+      wStatus.setImage(image);
+      wStatus.setText(message);
+      wStatus.setVisible(true);
+    }
   }
 
   /**
@@ -1062,21 +1109,49 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
   /* Update toolbar, button and menu state */
   public void updateGui() {
+    if (!isInitialized()) {
+      return;
+    }
+    if (wTree.getDisplay() != org.eclipse.swt.widgets.Display.getCurrent()) {
+      wTree.getDisplay().asyncExec(this::updateGui);
+      return;
+    }
     boolean gitEnabled = GitGuiPlugin.getInstance().getGit() != null;
-    boolean commitEnabled = gitEnabled && !wMessage.getText().trim().isEmpty();
-    wToolBar.setEnabled(gitEnabled);
-    wTree.setEnabled(gitEnabled);
-    wAmend.setEnabled(gitEnabled);
-    wMessage.setEnabled(gitEnabled);
+    boolean commitEnabled =
+        gitEnabled
+            && wMessage != null
+            && !wMessage.isDisposed()
+            && !wMessage.getText().trim().isEmpty();
+
+    if (wToolBar != null && !wToolBar.isDisposed()) {
+      wToolBar.setEnabled(gitEnabled);
+    }
+    if (wTree != null && !wTree.isDisposed()) {
+      wTree.setEnabled(gitEnabled);
+    }
+    if (wAmend != null && !wAmend.isDisposed()) {
+      wAmend.setEnabled(gitEnabled);
+    }
+    if (wMessage != null && !wMessage.isDisposed()) {
+      wMessage.setEnabled(gitEnabled);
+    }
 
-    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());
+    if (toolBarWidgets != null) {
+      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());
+    }
 
-    wStatus.setVisible(false);
-    wCommit.setEnabled(commitEnabled);
-    wCommitAndPush.setEnabled(commitEnabled);
+    if (wStatus != null && !wStatus.isDisposed()) {
+      wStatus.setVisible(false);
+    }
+    if (wCommit != null && !wCommit.isDisposed()) {
+      wCommit.setEnabled(commitEnabled);
+    }
+    if (wCommitAndPush != null && !wCommitAndPush.isDisposed()) {
+      wCommitAndPush.setEnabled(commitEnabled);
+    }
   }
 
   @GuiToolbarElement(
@@ -1087,10 +1162,14 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(key = SWT.F5)
   @GuiOsxKeyboardShortcut(key = SWT.F5)
   public void refresh() {
+    if (!isInitialized()) {
+      return;
+    }
+    if (wTree.getDisplay() != org.eclipse.swt.widgets.Display.getCurrent()) {
+      wTree.getDisplay().asyncExec(this::refresh);
+      return;
+    }
     try {
-      if (wTree == null || wTree.isDisposed()) {
-        return;
-      }
       wTree.setRedraw(false);
       wTree.removeAll();
 
@@ -1156,7 +1235,14 @@ public class GitCommitPerspective implements 
IHopPerspective {
   }
 
   protected Shell getShell() {
-    return hopGui.getShell();
+    if (wSashForm != null && !wSashForm.isDisposed()) {
+      return wSashForm.getShell();
+    }
+    if (hopGui != null) {
+      return hopGui.getShell();
+    }
+    HopGui currentHop = HopGui.peekInstance();
+    return currentHop != null ? currentHop.getShell() : null;
   }
 
   @Override
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 31e302bdfa..3e6853c468 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
@@ -157,6 +157,9 @@ public class GitGuiPlugin
   public void addRootChangedListener() {
     git = null;
     ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+    if (explorerPerspective == null) {
+      return;
+    }
 
     // Listener to what's going on in the explorer perspective...
     //
@@ -165,9 +168,38 @@ public class GitGuiPlugin
     explorerPerspective.getRefreshListeners().add(this);
     explorerPerspective.getSelectionListeners().add(this);
 
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null && hopGui.getShell() != null && 
!hopGui.getShell().isDisposed()) {
+      hopGui
+          .getShell()
+          .addListener(
+              SWT.Dispose,
+              e -> {
+                if (git != null) {
+                  try {
+                    git.closeRepo();
+                  } catch (Exception ignored) {
+                  }
+                  git = null;
+                }
+              });
+    }
+
     enableButtons();
   }
 
+  private void refreshGitPerspective(boolean refreshAll) {
+    GitPerspective gitPerspective = GitPerspective.getInstance();
+    if (gitPerspective != null && gitPerspective.isInitialized()) {
+      gitPerspective.refresh(refreshAll);
+    } else if (refreshAll) {
+      ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+      if (explorerPerspective != null) {
+        explorerPerspective.refresh();
+      }
+    }
+  }
+
   @GuiMenuElement(
       root = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
       parentId = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
@@ -188,7 +220,12 @@ public class GitGuiPlugin
     if (EnvironmentUtils.getInstance().isWeb()) {
       gitCommitOnWeb();
     } else {
-      GitCommitPerspective.getInstance().activate();
+      GitCommitPerspective perspective = GitCommitPerspective.getInstance();
+      if (perspective != null && perspective.isInitialized()) {
+        perspective.activate();
+      } else {
+        gitCommitOnWeb();
+      }
     }
   }
 
@@ -331,13 +368,16 @@ public class GitGuiPlugin
       label = "i18n::GitGuiPlugin.Menu.Branch.Pull.Text",
       image = "pull.svg")
   public void gitPull() {
+    if (git == null) {
+      return;
+    }
     try {
       boolean merged = git.pull();
 
       // Refresh the explorer file, refs and commit history. A pull fetches 
the remote refs even
       // when there was nothing to merge into the current branch.
       //
-      GitPerspective.getInstance().refresh(true);
+      refreshGitPerspective(true);
 
       if (merged) {
         MessageBox pullSuccessful =
@@ -545,12 +585,7 @@ public class GitGuiPlugin
 
     // 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);
-    }
+    refreshGitPerspective(true);
     enableButtons();
   }
 
@@ -617,12 +652,16 @@ public class GitGuiPlugin
           // Close the tabs of the files which were deleted
           //
           List<String> filenamesToClose = new ArrayList<>();
+          FileObject rootObj = HopVfs.getFileObject(git.getDirectory());
           for (String filePath : pathsToClean) {
-            if (!new File(git.getDirectory(), filePath).exists()) {
+            if (!rootObj.resolveFile(filePath).exists()) {
               filenamesToClose.add(filenames.get(filePath));
             }
           }
-          
ExplorerPerspective.getInstance().closeTabsForFilenames(filenamesToClose);
+          ExplorerPerspective explorer = ExplorerPerspective.getInstance();
+          if (explorer != null) {
+            explorer.closeTabsForFilenames(filenamesToClose);
+          }
 
           // Show confirmation message once after all files have been deleted
           MessageBox box =
@@ -642,12 +681,7 @@ public class GitGuiPlugin
 
     // 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);
-    }
+    refreshGitPerspective(true);
     enableButtons();
   }
 
@@ -658,16 +692,17 @@ public class GitGuiPlugin
    * @return The filename to match open tabs with
    */
   private String getOpenFilename(String relativePath) {
-    File file = new File(git.getDirectory(), relativePath);
+    if (git == null) {
+      return relativePath;
+    }
     try {
-      FileObject fileObject = HopVfs.getFileObject(file.getAbsolutePath());
-      if (fileObject.exists()) {
-        return HopVfs.getFilename(fileObject);
-      }
+      FileObject root = HopVfs.getFileObject(git.getDirectory());
+      FileObject fileObject = root.resolveFile(relativePath);
+      return HopVfs.getFilename(fileObject);
     } catch (Exception ignored) {
       // Fall back to the absolute filename below
     }
-    return file.getAbsolutePath();
+    return git.getDirectory() + "/" + relativePath;
   }
 
   @GuiMenuElement(
@@ -677,6 +712,9 @@ public class GitGuiPlugin
       label = "i18n::GitGuiPlugin.Menu.Branch.Create.Text",
       image = "branch-add.svg")
   public void gitCreateBranch() {
+    if (git == null) {
+      return;
+    }
     EnterStringDialog enterStringDialog =
         new EnterStringDialog(
             HopGui.getInstance().getShell(),
@@ -700,7 +738,7 @@ public class GitGuiPlugin
 
       // Refresh the git history, file explorer tree, change colors...
       //
-      GitPerspective.getInstance().refresh(true);
+      refreshGitPerspective(true);
     }
   }
 
@@ -711,6 +749,9 @@ public class GitGuiPlugin
       label = "i18n::GitGuiPlugin.Menu.Branch.Rename.Text",
       image = "ui/images/rename.svg")
   public void gitRenameBranch() {
+    if (git == null) {
+      return;
+    }
     String oldName =
         
HopGui.getInstance().getStatusToolbarWidgets().getToolbarItemText(ID_TOOLBAR_ITEM_GIT);
     EnterStringDialog enterStringDialog =
@@ -724,6 +765,7 @@ public class GitGuiPlugin
       boolean renamed = git.renameBranch(oldName, newName);
       if (renamed) {
         this.setBranchLabel(newName);
+        refreshGitPerspective(false);
       }
     }
   }
@@ -735,6 +777,9 @@ public class GitGuiPlugin
       label = "i18n::GitGuiPlugin.Menu.Branch.Merge.Text",
       image = "git-merge.svg")
   public void gitMergeBranch() {
+    if (git == null) {
+      return;
+    }
     List<String> branches = git.getBranches();
     EnterSelectionDialog selectionDialog =
         new EnterSelectionDialog(
@@ -767,16 +812,19 @@ public class GitGuiPlugin
 
       // Refresh the git history, file explorer tree, change colors...
       //
-      GitPerspective.getInstance().refresh(true);
+      refreshGitPerspective(true);
     }
   }
 
   private void gitCheckoutBranch(String name) {
+    if (git == null) {
+      return;
+    }
     git.checkout(name);
 
     // Refresh the git history, file explorer tree, change colors...
     //
-    GitPerspective.getInstance().refresh(true);
+    refreshGitPerspective(true);
   }
 
   @GuiMenuElement(
@@ -786,6 +834,9 @@ public class GitGuiPlugin
       label = "i18n::GitGuiPlugin.Menu.Branch.Delete.Text",
       image = "ui/images/delete.svg")
   public void gitDeleteBranch() {
+    if (git == null) {
+      return;
+    }
     List<String> branches = git.getBranches();
     EnterSelectionDialog selectionDialog =
         new EnterSelectionDialog(
@@ -810,7 +861,7 @@ public class GitGuiPlugin
 
       // Refresh the git history, file explorer tree, change colors...
       //
-      GitPerspective.getInstance().refresh(true);
+      refreshGitPerspective(true);
     }
   }
 
@@ -833,6 +884,9 @@ public class GitGuiPlugin
 
   private ExplorerFile getSelectedFile() {
     ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+    if (explorerPerspective == null) {
+      return null;
+    }
     return explorerPerspective.getSelectedFile();
   }
 
@@ -870,9 +924,15 @@ public class GitGuiPlugin
     enableButtons();
 
     // Refresh Git perspectives when a project is activated
-    GitPerspective.getInstance().refresh(false);
-    GitCommitPerspective.getInstance().retrieveState();
-    GitCommitPerspective.getInstance().refresh();
+    GitPerspective gitPerspective = GitPerspective.getInstance();
+    if (gitPerspective != null && gitPerspective.isInitialized()) {
+      gitPerspective.refresh(false);
+    }
+    GitCommitPerspective gitCommitPerspective = 
GitCommitPerspective.getInstance();
+    if (gitCommitPerspective != null && gitCommitPerspective.isInitialized()) {
+      gitCommitPerspective.retrieveState();
+      gitCommitPerspective.refresh();
+    }
   }
 
   private FileObject findGitConfig(String rootFolderName, boolean 
searchParentFolders)
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
index 6a0e58af45..1113944603 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
@@ -194,7 +194,7 @@ public class GitPerspective implements IHopPerspective {
 
   public static final String OPTION_SHOW_ALL_REF = "Git.ShowAllRef";
 
-  private static GitPerspective instance;
+  private static GitPerspective testFallback;
 
   private HopGui hopGui;
   private SashForm wSashForm;
@@ -225,19 +225,28 @@ public class GitPerspective implements IHopPerspective {
   private Timer fetchAutomaticTimer;
 
   public GitPerspective() {
-    instance = this;
+    if (HopGui.peekInstance() == null) {
+      testFallback = this;
+    }
   }
 
   public static GitPerspective getInstance() {
     try {
-      GitPerspective fromGui = 
HopGui.findSessionPerspective(GitPerspective.class);
-      if (fromGui != null) {
-        return fromGui;
+      if (HopGui.peekInstance() != null) {
+        return HopGui.findSessionPerspective(GitPerspective.class);
       }
     } catch (Throwable e) {
-      // No HopGuiImpl in unit tests
+      // No HopGui in headless unit tests
     }
-    return instance;
+    return testFallback;
+  }
+
+  /**
+   * Whether this perspective has been attached to a {@link HopGui} instance 
and its controls have
+   * been created and are still live.
+   */
+  public boolean isInitialized() {
+    return hopGui != null && wSashForm != null && !wSashForm.isDisposed();
   }
 
   @Override
@@ -249,7 +258,7 @@ public class GitPerspective implements IHopPerspective {
   @GuiOsxKeyboardShortcut(command = true, shift = true, key = 'g', global = 
true)
   @Override
   public void activate() {
-    if (hopGui == null) {
+    if (!isInitialized()) {
       // The perspective is switched off in disabledGuiElements.xml: it was 
never initialized, so
       // there is nothing to switch to. The git toolbar and menu items that 
lead here live on the
       // explorer perspective and are still there.
@@ -273,15 +282,20 @@ public class GitPerspective implements IHopPerspective {
 
   @Override
   public void perspectiveActivated() {
+    if (!isInitialized()) {
+      return;
+    }
     // TODO: avoid refresh when perspective is activated, detect change in the 
file explorer model
     refresh();
 
-    wSearchText.setFocus();
+    if (wSearchText != null && !wSearchText.isDisposed()) {
+      wSearchText.setFocus();
+    }
   }
 
   @Override
   public boolean isActive() {
-    return hopGui != null && hopGui.isActivePerspective(this);
+    return isInitialized() && hopGui.isActivePerspective(this);
   }
 
   @Override
@@ -312,12 +326,22 @@ public class GitPerspective implements IHopPerspective {
 
     // Add key listeners
     HopGuiKeyHandler.getInstance().addParentObjectToHandle(this);
+    wSashForm.addListener(
+        SWT.Dispose,
+        e -> {
+          stopFetchAutomaticTimer();
+          HopGuiKeyHandler.getInstance().removeParentObjectToHandle(this);
+        });
 
     hopGui
         .getEventsHandler()
         .addEventListener(
             getClass().getName() + "ProjectActivated",
-            e -> hopGui.getDisplay().asyncExec(this::clearSearchFilters),
+            e -> {
+              if (hopGui != null && !hopGui.getDisplay().isDisposed()) {
+                hopGui.getDisplay().asyncExec(this::clearSearchFilters);
+              }
+            },
             HopGuiEvents.ProjectActivated.name());
   }
 
@@ -525,14 +549,18 @@ public class GitPerspective implements IHopPerspective {
   }
 
   protected void refreshFiles() {
+    if (!isInitialized() || wFileTree == null || wFileTree.isDisposed()) {
+      return;
+    }
     try {
       setDiffText("");
       wFileTree.setRedraw(false);
       wFileTree.removeAll();
 
       RevCommit commit = getSelectedCommit();
-      if (commit != null) {
-        Git git = GitGuiPlugin.getInstance().getGit().getGit();
+      UIGit uiGit = GitGuiPlugin.getInstance().getGit();
+      if (commit != null && uiGit != null && uiGit.getGit() != null) {
+        Git git = uiGit.getGit();
         try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
           if (commit.getParentCount() > 0) {
             treeWalk.addTree(commit.getParent(0).getTree());
@@ -570,7 +598,9 @@ public class GitPerspective implements IHopPerspective {
         }
       }
 
-      wFileTree.setRedraw(true);
+      if (!wFileTree.isDisposed()) {
+        wFileTree.setRedraw(true);
+      }
 
       refreshFileDiff();
     } catch (Exception e) {
@@ -585,22 +615,25 @@ public class GitPerspective implements IHopPerspective {
 
     if (commit != null && fileName != null) {
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        String commitId = commit.name();
+        String parentCommitId = git.getParentCommitId(commitId);
+        String diff = git.diff(parentCommitId, commitId, fileName);
 
-      String commitId = commit.name();
-      String parentCommitId = git.getParentCommitId(commitId);
-      String diff = git.diff(parentCommitId, commitId, fileName);
-
-      setDiffText(diff);
+        setDiffText(diff);
 
-      // Enable visual diff button?
-      visualDiffEnabled = FileTypeUtils.isHopFileType(fileName);
+        // Enable visual diff button?
+        visualDiffEnabled = FileTypeUtils.isHopFileType(fileName);
+      }
     }
 
-    fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_SHOW_TEXT_DIFF, 
visualDiffEnabled);
+    if (fileToolBarWidgets != null) {
+      fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_SHOW_TEXT_DIFF, 
visualDiffEnabled);
+    }
   }
 
   public Ref getSelectedReference() {
-    if (wRefTree.getSelectionCount() < 1) {
+    if (wRefTree == null || wRefTree.isDisposed() || 
wRefTree.getSelectionCount() < 1) {
       return null;
     }
     TreeItem item = wRefTree.getSelection()[0];
@@ -612,7 +645,9 @@ public class GitPerspective implements IHopPerspective {
   }
 
   public RevCommit getSelectedCommit() {
-    if (wHistoryTable.getSelectionCount() < 1) {
+    if (wHistoryTable == null
+        || wHistoryTable.isDisposed()
+        || wHistoryTable.getSelectionCount() < 1) {
       return null;
     }
 
@@ -625,7 +660,7 @@ public class GitPerspective implements IHopPerspective {
   }
 
   public String getSelectedFile() {
-    if (wFileTree.getSelectionCount() < 1) {
+    if (wFileTree == null || wFileTree.isDisposed() || 
wFileTree.getSelectionCount() < 1) {
       return null;
     }
 
@@ -790,6 +825,9 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.ResetToCommit.Text",
       image = "git-reset.svg")
   public void resetToCommit() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
       ResetTypeDialog dialog = new ResetTypeDialog(getShell());
@@ -797,10 +835,10 @@ public class GitPerspective implements IHopPerspective {
 
       if (resetType != null) {
         UIGit git = GitGuiPlugin.getInstance().getGit();
-
-        git.reset(commit.getId().name(), resetType);
-
-        refresh(true);
+        if (git != null) {
+          git.reset(commit.getId().name(), resetType);
+          refresh(true);
+        }
       }
     }
   }
@@ -812,9 +850,15 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.RevertCommit.Text",
       image = "git-restore.svg")
   public void revertCommit() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
       UIGit uigit = GitGuiPlugin.getInstance().getGit();
+      if (uigit == null) {
+        return;
+      }
 
       // Check if the git repository is clean
       if (!uigit.isClean()) {
@@ -840,9 +884,10 @@ public class GitPerspective implements IHopPerspective {
 
       try {
         Git git = uigit.getGit();
-        RevCommit revertCommit = git.revert().include(commit).call();
-
-        refresh(true);
+        if (git != null) {
+          RevCommit revertCommit = git.revert().include(commit).call();
+          refresh(true);
+        }
       } catch (Exception e) {
         new ErrorDialog(
             getShell(),
@@ -867,6 +912,9 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.Toolbar.RevertFile.Tooltip",
       image = "git-restore.svg")
   public void revertFile() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     String path = getSelectedFile();
 
@@ -875,6 +923,9 @@ public class GitPerspective implements IHopPerspective {
     }
 
     UIGit git = GitGuiPlugin.getInstance().getGit();
+    if (git == null) {
+      return;
+    }
     String commitId = commit.getId().name();
 
     // Undo what the commit did to the file: put back the version of its parent
@@ -948,9 +999,15 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.Toolbar.CherryPickCommit.Tooltip",
       image = "cherry-pick.svg")
   public void cherryPickCommit() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
       UIGit uigit = GitGuiPlugin.getInstance().getGit();
+      if (uigit == null) {
+        return;
+      }
 
       // Check if the git repository is clean
       if (!uigit.isClean()) {
@@ -964,22 +1021,23 @@ public class GitPerspective implements IHopPerspective {
 
       try {
         Git git = uigit.getGit();
+        if (git != null) {
+          CherryPickResult result = 
git.cherryPick().include(commit).setNoCommit(false).call();
+
+          if (result.getStatus() == 
CherryPickResult.CherryPickStatus.CONFLICTING) {
+            MessageBox dialog = new MessageBox(getShell(), SWT.ICON_WARNING | 
SWT.YES | SWT.NO);
+            dialog.setText(
+                BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.CherryPickCommit.Header"));
+            dialog.setMessage(
+                BaseMessages.getString(
+                    PKG,
+                    "GitGuiPlugin.Dialog.CherryPickCommitConflicts.Message",
+                    commit,
+                    result.getFailingPaths()));
+          }
 
-        CherryPickResult result = 
git.cherryPick().include(commit).setNoCommit(false).call();
-
-        if (result.getStatus() == 
CherryPickResult.CherryPickStatus.CONFLICTING) {
-          MessageBox dialog = new MessageBox(getShell(), SWT.ICON_WARNING | 
SWT.YES | SWT.NO);
-          dialog.setText(
-              BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.CherryPickCommit.Header"));
-          dialog.setMessage(
-              BaseMessages.getString(
-                  PKG,
-                  "GitGuiPlugin.Dialog.CherryPickCommitConflicts.Message",
-                  commit,
-                  result.getFailingPaths()));
+          refresh(true);
         }
-
-        refresh(true);
       } catch (Exception e) {
         new ErrorDialog(
             getShell(),
@@ -1002,6 +1060,9 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.Toolbar.CherryPickFile.Tooltip",
       image = "cherry-pick.svg")
   public void cherryPickFile() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     String path = getSelectedFile();
 
@@ -1010,6 +1071,9 @@ public class GitPerspective implements IHopPerspective {
     }
 
     UIGit git = GitGuiPlugin.getInstance().getGit();
+    if (git == null) {
+      return;
+    }
     String commitId = commit.getId().name();
 
     if (!confirmFileAction(
@@ -1051,11 +1115,16 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.Checkout.Text",
       image = "ui/images/check.svg")
   public void checkoutReference() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = getSelectedReference();
     if (ref != null) {
-      GitGuiPlugin.getInstance().getGit().checkout(ref.getName());
-
-      refresh(true);
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        git.checkout(ref.getName());
+        refresh(true);
+      }
     }
   }
 
@@ -1067,11 +1136,16 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.CheckoutRevision.Text",
       image = "ui/images/check.svg")
   public void checkoutCommit() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
-      GitGuiPlugin.getInstance().getGit().checkout(commit.getName());
-
-      refresh(true);
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        git.checkout(commit.getName());
+        refresh(true);
+      }
     }
   }
 
@@ -1087,6 +1161,9 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.History.Toolbar.CreateTag.Tooltip",
       image = "tag-add.svg")
   public void addTag() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
       EnterStringDialog dialog =
@@ -1099,7 +1176,7 @@ public class GitPerspective implements IHopPerspective {
       String name = dialog.open();
       if (name != null) {
         UIGit git = GitGuiPlugin.getInstance().getGit();
-        if (git.createTag(name, commit.getId().name())) {
+        if (git != null && git.createTag(name, commit.getId().name())) {
           refresh(false);
         }
       }
@@ -1119,6 +1196,9 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.History.Toolbar.CreateBranch.Tooltip",
       image = "branch-add.svg")
   public void addBranchFromCommit() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     if (commit != null) {
       EnterStringDialog dialog =
@@ -1131,7 +1211,7 @@ public class GitPerspective implements IHopPerspective {
       String name = dialog.open();
       if (name != null) {
         UIGit git = GitGuiPlugin.getInstance().getGit();
-        if (git.createBranch(name, commit.getId().name())) {
+        if (git != null && git.createBranch(name, commit.getId().name())) {
           // Refresh the explorer file, refs and commit history
           refresh(true);
         }
@@ -1147,6 +1227,9 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.CreateBranchFrom.Text",
       image = "branch-add.svg")
   public void addBranchFromRef() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = getSelectedReference();
     if (ref != null) {
       EnterStringDialog dialog =
@@ -1159,7 +1242,7 @@ public class GitPerspective implements IHopPerspective {
       String name = dialog.open();
       if (name != null) {
         UIGit git = GitGuiPlugin.getInstance().getGit();
-        if (git.createBranch(name, ref.getName())) {
+        if (git != null && git.createBranch(name, ref.getName())) {
           // Refresh the explorer file, refs and commit history
           refresh(true);
         }
@@ -1174,15 +1257,20 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.MergeInto.Text",
       image = "git-merge.svg")
   public void mergeBranch() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = getSelectedReference();
     if (ref != null) {
       try {
         UIGit git = GitGuiPlugin.getInstance().getGit();
-        String name = git.getShortenedName(ref.getName());
-        git.mergeBranch(name, MergeStrategy.RECURSIVE);
+        if (git != null) {
+          String name = git.getShortenedName(ref.getName());
+          git.mergeBranch(name, MergeStrategy.RECURSIVE);
 
-        // Refresh the explorer file, refs and commit history
-        refresh(true);
+          // Refresh the explorer file, refs and commit history
+          refresh(true);
+        }
       } catch (Exception e) {
         new ErrorDialog(
             getShell(),
@@ -1207,11 +1295,17 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.History.Toolbar.Fetch.Tooltip",
       image = "fetch.svg")
   public void fetch() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
-      GitGuiPlugin.getInstance().getGit().fetch();
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        git.fetch();
 
-      // Refresh refs and commit history
-      refresh(false);
+        // Refresh refs and commit history
+        refresh(false);
+      }
     } catch (Exception e) {
       new ErrorDialog(
           getShell(),
@@ -1235,11 +1329,17 @@ public class GitPerspective implements IHopPerspective {
   @GuiKeyboardShortcut(control = true, key = 'T')
   @GuiOsxKeyboardShortcut(control = true, key = 'T')
   public void pull() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
-      GitGuiPlugin.getInstance().getGit().pull();
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        git.pull();
 
-      // Refresh the explorer file, refs and commit history
-      refresh(true);
+        // Refresh the explorer file, refs and commit history
+        refresh(true);
+      }
     } catch (Exception e) {
       new ErrorDialog(
           getShell(),
@@ -1255,11 +1355,17 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitGuiPlugin.Toolbar.Push.Tooltip",
       image = "push.svg")
   public void push() {
+    if (!isInitialized()) {
+      return;
+    }
     try {
-      GitGuiPlugin.getInstance().getGit().push();
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git != null) {
+        git.push();
 
-      // Refresh refs and commit history
-      refresh(false);
+        // Refresh refs and commit history
+        refresh(false);
+      }
     } catch (Exception e) {
       new ErrorDialog(
           getShell(),
@@ -1277,17 +1383,22 @@ public class GitPerspective implements IHopPerspective {
       label = "i18n::GitPerspective.Menu.Push.Text",
       image = "push.svg")
   public void pushReference() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = getSelectedReference();
     if (ref == null) {
       return;
     }
     try {
       UIGit git = GitGuiPlugin.getInstance().getGit();
-      String type = ref.getName().startsWith(Constants.R_TAGS) ? VCS.TYPE_TAG 
: VCS.TYPE_BRANCH;
-      git.push(type, git.getShortenedName(ref.getName()));
+      if (git != null) {
+        String type = ref.getName().startsWith(Constants.R_TAGS) ? 
VCS.TYPE_TAG : VCS.TYPE_BRANCH;
+        git.push(type, git.getShortenedName(ref.getName()));
 
-      // Refresh refs and commit history
-      refresh(false);
+        // Refresh refs and commit history
+        refresh(false);
+      }
     } catch (Exception e) {
       new ErrorDialog(
           getShell(),
@@ -1307,19 +1418,26 @@ public class GitPerspective implements IHopPerspective {
   @GuiKeyboardShortcut(key = SWT.F2)
   @GuiOsxKeyboardShortcut(key = SWT.F2)
   public void renameReference() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = this.getSelectedReference();
-    if (ref != null) {
+    if (ref != null && wRefTree.getSelectionCount() > 0) {
       boolean isRemote = ref.getName().startsWith(Constants.R_REMOTES);
       if (!isRemote && !ref.getName().startsWith(Constants.R_HEADS)) {
         // Only branches can be renamed
         return;
       }
-      if (isRemote && 
GitGuiPlugin.getInstance().getGit().isRemoteHead(ref.getName())) {
+      UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
+      if (isRemote && git.isRemoteHead(ref.getName())) {
         return;
       }
 
-      UIGit git = GitGuiPlugin.getInstance().getGit();
       String oldName = git.getShortenedName(ref.getName());
+      TreeItem selectedItem = wRefTree.getSelection()[0];
 
       // The control that will be the editor must be a child of the Tree
       Text text = new Text(wRefTree, SWT.BORDER);
@@ -1358,7 +1476,7 @@ public class GitPerspective implements IHopPerspective {
       text.selectAll();
       text.setFocus();
       PropsUi.setLook(text);
-      wRefTreeEditor.setEditor(text, wRefTree.getSelection()[0]);
+      wRefTreeEditor.setEditor(text, selectedItem);
     }
   }
 
@@ -1371,6 +1489,9 @@ public class GitPerspective implements IHopPerspective {
   @GuiKeyboardShortcut(key = SWT.DEL)
   @GuiOsxKeyboardShortcut(key = SWT.DEL)
   public void deleteReference() {
+    if (!isInitialized()) {
+      return;
+    }
     Ref ref = this.getSelectedReference();
     if (ref != null) {
       if (ref.getName().startsWith(Constants.R_HEADS)) {
@@ -1386,6 +1507,9 @@ public class GitPerspective implements IHopPerspective {
   protected void deleteBranch(Ref ref) {
     if (ref != null) {
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       String name = git.getShortenedName(ref.getName());
 
       // Prevent deletion of the current branch
@@ -1418,6 +1542,9 @@ public class GitPerspective implements IHopPerspective {
       return;
     }
     UIGit git = GitGuiPlugin.getInstance().getGit();
+    if (git == null) {
+      return;
+    }
     String name = git.getShortenedName(ref.getName());
 
     // The default branch of the remote is protected against an accidental 
delete
@@ -1462,6 +1589,9 @@ public class GitPerspective implements IHopPerspective {
    */
   private void renameRemoteBranch(Ref ref, String oldName, String newName) {
     UIGit git = GitGuiPlugin.getInstance().getGit();
+    if (git == null) {
+      return;
+    }
 
     // The tree shows remote branches with their remote in front of them. 
Accept a new name with
     // or without it, the branch is renamed on the same remote either way.
@@ -1522,6 +1652,9 @@ public class GitPerspective implements IHopPerspective {
   protected void deleteTag(Ref ref) {
     if (ref != null) {
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       String name = git.getShortenedName(ref.getName());
 
       MessageBox dialog = new MessageBox(getShell(), SWT.ICON_QUESTION | 
SWT.YES | SWT.NO);
@@ -1588,15 +1721,20 @@ public class GitPerspective implements IHopPerspective {
       toolTip = "i18n::GitPerspective.Toolbar.ShowHiddenAllRef.Tooltip",
       image = "ui/images/hide.svg")
   public void showAllRef() {
+    if (!isInitialized()) {
+      return;
+    }
     showAllRef = !showAllRef;
 
     // Update toolbar item icon
-    ToolItem toolItem = 
this.historyToolBarWidgets.findToolItem(TOOLBAR_ITEM_SHOW_HIDDEN);
-    if (toolItem != null) {
-      if (showAllRef) {
-        toolItem.setImage(GuiResource.getInstance().getImageShow());
-      } else {
-        toolItem.setImage(GuiResource.getInstance().getImageHide());
+    if (this.historyToolBarWidgets != null) {
+      ToolItem toolItem = 
this.historyToolBarWidgets.findToolItem(TOOLBAR_ITEM_SHOW_HIDDEN);
+      if (toolItem != null && !toolItem.isDisposed()) {
+        if (showAllRef) {
+          toolItem.setImage(GuiResource.getInstance().getImageShow());
+        } else {
+          toolItem.setImage(GuiResource.getInstance().getImageHide());
+        }
       }
     }
 
@@ -1620,6 +1758,9 @@ public class GitPerspective implements IHopPerspective {
   @GuiKeyboardShortcut(control = true, key = 'D')
   @GuiOsxKeyboardShortcut(control = true, key = 'D')
   public void showTextDiff() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     String fileName = getSelectedFile();
 
@@ -1629,6 +1770,9 @@ public class GitPerspective implements IHopPerspective {
 
     try {
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       String commitIdNew = commit.getId().name();
       String commitIdOld = git.getParentCommitId(commitIdNew);
 
@@ -1641,8 +1785,7 @@ public class GitPerspective implements IHopPerspective {
 
       GitGuiPlugin.getInstance().showTextFileDiff(fileName, commitIdNew, 
commitIdOld);
     } catch (Exception e) {
-      new ErrorDialog(
-          hopGui.getShell(), "Error", "Error while doing text diff on file : " 
+ fileName, e);
+      new ErrorDialog(getShell(), "Error", "Error while doing text diff on 
file : " + fileName, e);
     }
   }
 
@@ -1660,6 +1803,9 @@ public class GitPerspective implements IHopPerspective {
   @GuiKeyboardShortcut(control = true, shift = true, key = 'D')
   @GuiOsxKeyboardShortcut(control = true, shift = true, key = 'D')
   public void showGraphDiff() {
+    if (!isInitialized()) {
+      return;
+    }
     RevCommit commit = getSelectedCommit();
     String fileName = getSelectedFile();
 
@@ -1669,6 +1815,9 @@ public class GitPerspective implements IHopPerspective {
 
     try {
       UIGit git = GitGuiPlugin.getInstance().getGit();
+      if (git == null) {
+        return;
+      }
       String commitIdNew = commit.getId().name();
       String commitIdOld = git.getParentCommitId(commitIdNew);
 
@@ -1680,17 +1829,21 @@ public class GitPerspective implements IHopPerspective {
       }
 
       ExplorerPerspective perspective = HopGui.getExplorerPerspective();
-      if (perspective.getPipelineFileType().isHandledBy(fileName, false)) {
+      if (perspective != null
+          && perspective.getPipelineFileType() != null
+          && perspective.getPipelineFileType().isHandledBy(fileName, false)) {
         // A pipeline
         GitGuiPlugin.getInstance().showPipelineFileDiff(fileName, commitIdNew, 
commitIdOld);
-      } else if (perspective.getWorkflowFileType().isHandledBy(fileName, 
false)) {
+      } else if (perspective != null
+          && perspective.getWorkflowFileType() != null
+          && perspective.getWorkflowFileType().isHandledBy(fileName, false)) {
         // A workflow
         GitGuiPlugin.getInstance().showWorkflowFileDiff(fileName, commitIdNew, 
commitIdOld);
       }
 
     } catch (Exception e) {
       new ErrorDialog(
-          hopGui.getShell(), "Error", "Error while doing visual diff on file : 
" + fileName, e);
+          getShell(), "Error", "Error while doing visual diff on file : " + 
fileName, e);
     }
   }
 
@@ -1718,6 +1871,13 @@ public class GitPerspective implements IHopPerspective {
   }
 
   public void updateGui() {
+    if (!isInitialized()) {
+      return;
+    }
+    if (wHistoryTable.getDisplay() != 
org.eclipse.swt.widgets.Display.getCurrent()) {
+      wHistoryTable.getDisplay().asyncExec(this::updateGui);
+      return;
+    }
     UIGit git = GitGuiPlugin.getInstance().getGit();
 
     boolean isGitEnabled = git != null;
@@ -1734,27 +1894,41 @@ public class GitPerspective implements IHopPerspective {
       }
     }
 
-    wRefTree.setEnabled(isGitEnabled);
-    wHistoryToolBar.setEnabled(isGitEnabled);
-    wHistoryTable.setEnabled(isGitEnabled);
-    wFileToolBar.setEnabled(isGitEnabled);
-    wFileTree.setEnabled(isGitEnabled);
+    if (wRefTree != null && !wRefTree.isDisposed()) {
+      wRefTree.setEnabled(isGitEnabled);
+    }
+    if (wHistoryToolBar != null && !wHistoryToolBar.isDisposed()) {
+      wHistoryToolBar.setEnabled(isGitEnabled);
+    }
+    if (wHistoryTable != null && !wHistoryTable.isDisposed()) {
+      wHistoryTable.setEnabled(isGitEnabled);
+    }
+    if (wFileToolBar != null && !wFileToolBar.isDisposed()) {
+      wFileToolBar.setEnabled(isGitEnabled);
+    }
+    if (wFileTree != null && !wFileTree.isDisposed()) {
+      wFileTree.setEnabled(isGitEnabled);
+    }
 
-    historyToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CREATE_BRANCH, 
isCommitSelected);
-    historyToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CREATE_TAG, 
isCommitSelected);
-    historyToolBarWidgets.enableToolbarItem(
-        TOOLBAR_ITEM_COMMIT_CHERRY_PICK, isCommitSelected && 
!isCommitInCurrentBranch);
+    if (historyToolBarWidgets != null) {
+      historyToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CREATE_BRANCH, 
isCommitSelected);
+      historyToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CREATE_TAG, 
isCommitSelected);
+      historyToolBarWidgets.enableToolbarItem(
+          TOOLBAR_ITEM_COMMIT_CHERRY_PICK, isCommitSelected && 
!isCommitInCurrentBranch);
+    }
 
     String selectFile = getSelectedFile();
     boolean isFileSelected = selectFile != null;
-    fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_SHOW_TEXT_DIFF, 
isFileSelected);
-    fileToolBarWidgets.enableToolbarItem(
-        TOOLBAR_ITEM_FILE_SHOW_GRAPH_DIFF, 
FileTypeUtils.isHopFileType(selectFile));
-    // Reverting a file puts back the version of the commit's parent, which a 
root commit does not
-    // have and a merge commit has more than one of
-    fileToolBarWidgets.enableToolbarItem(
-        TOOLBAR_ITEM_FILE_REVERT, isFileSelected && hasSingleParent);
-    fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_CHERRY_PICK, 
isFileSelected);
+    if (fileToolBarWidgets != null) {
+      fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_SHOW_TEXT_DIFF, 
isFileSelected);
+      fileToolBarWidgets.enableToolbarItem(
+          TOOLBAR_ITEM_FILE_SHOW_GRAPH_DIFF, 
FileTypeUtils.isHopFileType(selectFile));
+      // Reverting a file puts back the version of the commit's parent, which 
a root commit does not
+      // have and a merge commit has more than one of
+      fileToolBarWidgets.enableToolbarItem(
+          TOOLBAR_ITEM_FILE_REVERT, isFileSelected && hasSingleParent);
+      fileToolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_FILE_CHERRY_PICK, 
isFileSelected);
+    }
   }
 
   protected void createHistoryTable(Composite parent) {
@@ -1919,11 +2093,14 @@ public class GitPerspective implements IHopPerspective {
    *     should be refreshed.
    */
   public void refresh(boolean refreshAll) {
+    if (!isInitialized()) {
+      return;
+    }
+    if (wHistoryTable.getDisplay() != 
org.eclipse.swt.widgets.Display.getCurrent()) {
+      wHistoryTable.getDisplay().asyncExec(() -> refresh(refreshAll));
+      return;
+    }
     try {
-      if (wHistoryTable == null || wHistoryTable.isDisposed()) {
-        return;
-      }
-
       UIGit uiGit = GitGuiPlugin.getInstance().getGit();
 
       // Drop UI state bound to the previous repository before reading 
selection / rebuilding.
@@ -1939,8 +2116,14 @@ public class GitPerspective implements IHopPerspective {
       // Refresh the file explorer perspective (file tree, change colors...)
       // Refresh the metadata perspective (if file metadata provider)
       if (refreshAll) {
-        ExplorerPerspective.getInstance().refresh();
-        MetadataPerspective.getInstance().refresh();
+        ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+        if (explorerPerspective != null) {
+          explorerPerspective.refresh();
+        }
+        MetadataPerspective metadataPerspective = 
MetadataPerspective.getInstance();
+        if (metadataPerspective != null) {
+          metadataPerspective.refresh();
+        }
       }
 
       refreshRef(uiGit.getGit());
@@ -1969,11 +2152,15 @@ public class GitPerspective implements IHopPerspective {
   }
 
   protected void refreshRef(Git git) {
+    if (!isInitialized() || wRefTree == null || wRefTree.isDisposed() || git 
== null) {
+      return;
+    }
     try {
       wRefTree.setRedraw(false);
       wRefTree.removeAll();
 
-      String currentBranch = GitGuiPlugin.getInstance().getGit().getBranch();
+      UIGit uiGit = GitGuiPlugin.getInstance().getGit();
+      String currentBranch = uiGit != null ? uiGit.getBranch() : null;
 
       // Local branches
       TreeItem localItem = new TreeItem(wRefTree, SWT.NONE);
@@ -2018,7 +2205,9 @@ public class GitPerspective implements IHopPerspective {
 
       TreeMemory.setExpandedFromMemory(wRefTree, GIT_PERSPECTIVE_REF_TREE);
 
-      wRefTree.setRedraw(true);
+      if (!wRefTree.isDisposed()) {
+        wRefTree.setRedraw(true);
+      }
     } catch (Exception e) {
       LogChannel.UI.logError("Error refresh git commit history", e);
     }
@@ -2056,104 +2245,132 @@ public class GitPerspective implements 
IHopPerspective {
   }
 
   protected void refreshHistory(String startRef) {
-    Git git = GitGuiPlugin.getInstance().getGit().getGit();
+    if (!isInitialized()) {
+      return;
+    }
+    UIGit uiGit = GitGuiPlugin.getInstance().getGit();
+    if (uiGit == null) {
+      return;
+    }
+    Git git = uiGit.getGit();
+    if (git == null) {
+      return;
+    }
 
     DateTimeFormatter formatter =
         DateTimeFormatter.ofPattern("yyyy/MM/dd 
HH:mm").withZone(ZoneId.systemDefault());
 
     try {
       Repository repository = git.getRepository();
-      PlotWalk walk = new PlotWalk(repository);
+      try (PlotWalk walk = new PlotWalk(repository)) {
 
-      ObjectId startCommitId = null;
-      if (!Utils.isEmpty(startRef)) {
-        startCommitId = repository.resolve(startRef);
-      }
+        ObjectId startCommitId = null;
+        if (!Utils.isEmpty(startRef)) {
+          startCommitId = repository.resolve(startRef);
+        }
 
-      List<RevCommit> marks = new ArrayList<>();
+        List<RevCommit> marks = new ArrayList<>();
 
-      // Always show HEAD
-      marks.add(walk.parseCommit(repository.resolve(Constants.HEAD)));
+        // Always show HEAD
+        ObjectId headId = repository.resolve(Constants.HEAD);
+        if (headId != null) {
+          marks.add(walk.parseCommit(headId));
+        }
 
-      // Show add all branches and tags
-      if (showAllRef) {
-        for (Ref ref : git.branchList().call()) {
-          RevCommit commit = walk.parseCommit(ref.getObjectId());
-          if (!marks.contains(commit)) {
-            marks.add(commit);
+        // Show add all branches and tags
+        if (showAllRef) {
+          for (Ref ref : git.branchList().call()) {
+            RevCommit commit = walk.parseCommit(ref.getObjectId());
+            if (!marks.contains(commit)) {
+              marks.add(commit);
+            }
           }
+        } else if (startCommitId != null) {
+          marks.add(walk.parseCommit(startCommitId));
         }
-      } else if (startCommitId != null) {
-        marks.add(walk.parseCommit(startCommitId));
-      }
 
-      walk.markStart(marks);
+        if (!marks.isEmpty()) {
+          walk.markStart(marks);
+        }
 
-      SwtCommitList plotList = new SwtCommitList();
-      plotList.source(walk);
-      plotList.fillTo(Integer.MAX_VALUE);
+        SwtCommitList plotList = new SwtCommitList();
+        plotList.source(walk);
+        plotList.fillTo(Integer.MAX_VALUE);
 
-      wHistoryTable.setRedraw(false);
-      wHistoryTable.removeAll();
+        if (wHistoryTable != null && !wHistoryTable.isDisposed()) {
+          wHistoryTable.setRedraw(false);
+          wHistoryTable.removeAll();
 
-      // Check if there are changes in the working directory
-      Map<String, UIFile> filesToCommit = 
GitGuiPlugin.getInstance().getChangedFiles();
-      if (!filesToCommit.isEmpty()) {
-        TableItem item = new TableItem(wHistoryTable, SWT.NONE);
-        item.setText(
-            1,
-            BaseMessages.getString(
-                PKG, "GitPerspective.History.UncommittedChanges.Label", 
filesToCommit.size()));
-        item.setForeground(GuiResource.getInstance().getColorGray());
-      }
+          // Check if there are changes in the working directory
+          Map<String, UIFile> filesToCommit = 
GitGuiPlugin.getInstance().getChangedFiles();
+          if (filesToCommit != null && !filesToCommit.isEmpty()) {
+            TableItem item = new TableItem(wHistoryTable, SWT.NONE);
+            item.setText(
+                1,
+                BaseMessages.getString(
+                    PKG, "GitPerspective.History.UncommittedChanges.Label", 
filesToCommit.size()));
+            item.setForeground(GuiResource.getInstance().getColorGray());
+          }
 
-      String search = wSearchText.getText().toLowerCase().trim();
+          String search =
+              (wSearchText != null && !wSearchText.isDisposed())
+                  ? wSearchText.getText().toLowerCase().trim()
+                  : "";
+
+          TableItem selectedItem = null;
+          for (PlotCommit<SwtCommitList.Lane> commit : plotList) {
+            String message = commit.getShortMessage();
+            String author = commit.getAuthorIdent().getName();
+            String id = 
commit.getId().abbreviate(Constants.OBJECT_ID_ABBREV_STRING_LENGTH).name();
+
+            // Apply filter if the search text is not empty
+            if (!search.isEmpty()
+                && !message.toLowerCase().contains(search)
+                && !author.toLowerCase().contains(search)
+                && !id.toLowerCase().contains(search)) {
+              continue;
+            }
 
-      TableItem selectedItem = null;
-      for (PlotCommit<SwtCommitList.Lane> commit : plotList) {
-        String message = commit.getShortMessage();
-        String author = commit.getAuthorIdent().getName();
-        String id = 
commit.getId().abbreviate(Constants.OBJECT_ID_ABBREV_STRING_LENGTH).name();
+            TableItem item = new TableItem(wHistoryTable, SWT.NONE);
+            item.setText(1, message);
+            item.setText(2, author);
+            item.setText(
+                3,
+                formatter
+                    .withZone(commit.getCommitterIdent().getZoneId())
+                    .format(commit.getCommitterIdent().getWhenAsInstant()));
+            item.setText(4, id);
+            item.setData(commit);
+
+            if (startCommitId != null && commit.getId().equals(startCommitId)) 
{
+              selectedItem = item;
+            }
+          }
 
-        // Apply filter if the search text is not empty
-        if (!search.isEmpty()
-            && !message.toLowerCase().contains(search)
-            && !author.toLowerCase().contains(search)
-            && !id.toLowerCase().contains(search)) {
-          continue;
-        }
+          if (selectedItem != null) {
+            wHistoryTable.setSelection(selectedItem);
+          }
 
-        TableItem item = new TableItem(wHistoryTable, SWT.NONE);
-        item.setText(1, message);
-        item.setText(2, author);
-        item.setText(
-            3,
-            formatter
-                .withZone(commit.getCommitterIdent().getZoneId())
-                .format(commit.getCommitterIdent().getWhenAsInstant()));
-        item.setText(4, id);
-        item.setData(commit);
-
-        if (commit.getId().equals(startCommitId)) {
-          selectedItem = item;
+          wHistoryTable.setRedraw(true);
+          wHistoryTable.redraw();
         }
-      }
 
-      if (selectedItem != null) {
-        wHistoryTable.setSelection(selectedItem);
+        refreshFiles();
       }
-
-      wHistoryTable.setRedraw(true);
-      wHistoryTable.redraw();
-
-      refreshFiles();
     } catch (Exception e) {
       LogChannel.UI.logError("Error refresh git history", e);
     }
   }
 
   protected Shell getShell() {
-    return hopGui.getShell();
+    if (wSashForm != null && !wSashForm.isDisposed()) {
+      return wSashForm.getShell();
+    }
+    if (hopGui != null) {
+      return hopGui.getShell();
+    }
+    HopGui currentHop = HopGui.peekInstance();
+    return currentHop != null ? currentHop.getShell() : null;
   }
 
   @Override
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigSingleton.java
 
b/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigSingleton.java
index 124852af3d..9336de776f 100644
--- 
a/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigSingleton.java
+++ 
b/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigSingleton.java
@@ -57,20 +57,19 @@ public class GitConfigSingleton {
     
HopConfig.getInstance().getConfigMap().put(GitConfig.HOP_CONFIG_GIT_CONFIG_KEY, 
gitConfig);
   }
 
-  public static GitConfigSingleton getInstance() {
-    return configSingleton;
-  }
-
-  public static GitConfig getConfig() {
+  public static synchronized GitConfigSingleton getInstance() {
     if (configSingleton == null) {
       configSingleton = new GitConfigSingleton();
     }
-    return configSingleton.gitConfig;
+    return configSingleton;
+  }
+
+  public static synchronized GitConfig getConfig() {
+    return getInstance().gitConfig;
   }
 
-  public static void saveConfig() throws HopException {
-    HopConfig.getInstance()
-        .saveOption(GitConfig.HOP_CONFIG_GIT_CONFIG_KEY, 
configSingleton.gitConfig);
+  public static synchronized void saveConfig() throws HopException {
+    HopConfig.getInstance().saveOption(GitConfig.HOP_CONFIG_GIT_CONFIG_KEY, 
getConfig());
     HopConfig.getInstance().saveToFile();
   }
 }
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
index 32d2014f16..7b3fbb5a58 100644
--- 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
+++ 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
@@ -372,6 +372,9 @@ public class GitInfoExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
     if (wFiles.getSelectionIndices().length == 0) {
       return;
     }
+    if (wRevisions.table.getSelectionCount() == 0) {
+      return;
+    }
     TableItem fileItem = wFiles.table.getSelection()[0];
     String filename = fileItem.getText(1);
     if (StringUtils.isEmpty(filename)) {
@@ -380,6 +383,9 @@ public class GitInfoExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
 
     GitGuiPlugin guiPlugin = GitGuiPlugin.getInstance();
     UIGit git = guiPlugin.getGit();
+    if (git == null) {
+      return;
+    }
 
     try {
 
@@ -415,11 +421,15 @@ public class GitInfoExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
       }
 
       ExplorerPerspective perspective = HopGui.getExplorerPerspective();
-      if (perspective.getPipelineFileType().isHandledBy(filename, false)) {
+      if (perspective != null
+          && perspective.getPipelineFileType() != null
+          && perspective.getPipelineFileType().isHandledBy(filename, false)) {
         // A pipeline
         //
         showPipelineFileDiff(filename, commitIdNew, commitIdOld);
-      } else if (perspective.getWorkflowFileType().isHandledBy(filename, 
false)) {
+      } else if (perspective != null
+          && perspective.getWorkflowFileType() != null
+          && perspective.getWorkflowFileType().isHandledBy(filename, false)) {
         // A workflow
         //
         showWorkflowFileDiff(filename, commitIdNew, commitIdOld);
diff --git 
a/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
 
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
index a59f66edc3..b91161068d 100644
--- 
a/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
+++ 
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
@@ -17,10 +17,13 @@
 
 package org.apache.hop.git;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import org.apache.hop.git.config.GitConfigSingleton;
 import org.junit.jupiter.api.Test;
 
 class GitGuiPluginSessionIsolationTest {
@@ -52,4 +55,88 @@ class GitGuiPluginSessionIsolationTest {
       }
     }
   }
+
+  @Test
+  void gitPerspectiveIsNotAProcessWideStaticSingletonField() {
+    for (Field field : GitPerspective.class.getDeclaredFields()) {
+      if ("instance".equals(field.getName())) {
+        assertFalse(
+            Modifier.isStatic(field.getModifiers()),
+            "GitPerspective must not keep a process-wide static instance 
field");
+      }
+    }
+  }
+
+  @Test
+  void gitCommitPerspectiveIsNotAProcessWideStaticSingletonField() {
+    for (Field field : GitCommitPerspective.class.getDeclaredFields()) {
+      if ("instance".equals(field.getName())) {
+        assertFalse(
+            Modifier.isStatic(field.getModifiers()),
+            "GitCommitPerspective must not keep a process-wide static instance 
field");
+      }
+    }
+  }
+
+  @Test
+  void gitCommitPerspectiveUninitializedIsSafe() {
+    GitCommitPerspective perspective = new GitCommitPerspective();
+    assertFalse(perspective.isInitialized());
+    assertDoesNotThrow(perspective::activate);
+    assertDoesNotThrow(perspective::perspectiveActivated);
+    assertDoesNotThrow(perspective::retrieveState);
+    assertDoesNotThrow(perspective::saveState);
+    assertDoesNotThrow(perspective::refresh);
+    assertDoesNotThrow(perspective::updateGui);
+    assertDoesNotThrow(perspective::selectAllChanged);
+    assertDoesNotThrow(perspective::addFilesToGit);
+    assertDoesNotThrow(perspective::unstageFiles);
+    assertDoesNotThrow(perspective::addFilesToGitIgnore);
+    assertDoesNotThrow(perspective::deleteFiles);
+    assertDoesNotThrow(perspective::restoreFiles);
+    assertDoesNotThrow(perspective::showTextDiff);
+    assertDoesNotThrow(perspective::showGraphDiff);
+    assertDoesNotThrow(() -> perspective.commitFiles(false));
+  }
+
+  @Test
+  void gitPerspectiveUninitializedIsSafe() {
+    GitPerspective perspective = new GitPerspective();
+    assertFalse(perspective.isInitialized());
+    assertDoesNotThrow(perspective::activate);
+    assertDoesNotThrow(perspective::perspectiveActivated);
+    assertDoesNotThrow(() -> perspective.refresh());
+    assertDoesNotThrow(() -> perspective.refresh(true));
+    assertDoesNotThrow(perspective::updateGui);
+    assertDoesNotThrow(perspective::clearGitUiState);
+    assertDoesNotThrow(perspective::clearSearchFilters);
+    assertDoesNotThrow(perspective::copyRevisionId);
+    assertDoesNotThrow(perspective::copyPath);
+    assertDoesNotThrow(perspective::resetToCommit);
+    assertDoesNotThrow(perspective::revertCommit);
+    assertDoesNotThrow(perspective::revertFile);
+    assertDoesNotThrow(perspective::cherryPickCommit);
+    assertDoesNotThrow(perspective::cherryPickFile);
+    assertDoesNotThrow(perspective::checkoutReference);
+    assertDoesNotThrow(perspective::checkoutCommit);
+    assertDoesNotThrow(perspective::addTag);
+    assertDoesNotThrow(perspective::addBranchFromCommit);
+    assertDoesNotThrow(perspective::addBranchFromRef);
+    assertDoesNotThrow(perspective::mergeBranch);
+    assertDoesNotThrow(perspective::fetch);
+    assertDoesNotThrow(perspective::pull);
+    assertDoesNotThrow(perspective::push);
+    assertDoesNotThrow(perspective::pushReference);
+    assertDoesNotThrow(perspective::renameReference);
+    assertDoesNotThrow(perspective::deleteReference);
+    assertDoesNotThrow(perspective::showAllRef);
+    assertDoesNotThrow(perspective::showTextDiff);
+    assertDoesNotThrow(perspective::showGraphDiff);
+  }
+
+  @Test
+  void gitConfigSingletonThreadSafe() {
+    assertNotNull(GitConfigSingleton.getInstance());
+    assertNotNull(GitConfigSingleton.getConfig());
+  }
 }

Reply via email to