This is an automated email from the ASF dual-hosted git repository.
bamaer 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 f52474c763 Add a “Revisions” tab, which lists all revisions of a
workflow or pipeline #8754 (#8761)
f52474c763 is described below
commit f52474c7638dcc09a5472cd8637b3c3bbbaf86e1
Author: Nicolas Adment <[email protected]>
AuthorDate: Thu Oct 8 19:13:14 2026 +0200
Add a “Revisions” tab, which lists all revisions of a workflow or pipeline
#8754 (#8761)
* Add a “Revisions” tab, which lists all revisions of a workflow or
pipeline #8754
- `BaseRevisionDelegate` contains the shared logic: tab creation, history
loading and diff handling.
- `PipelineRevisionDelegate` and `WorkflowRevisionDelegate` add the tab to
`HopGuiPipelineGraph` and HopGuiWorkflowGraph` through `@GuiTab`, and declare
their own toolbars with `@GuiToolbarElement`.
* Address review remarks on the “Revisions” tab #8754
- Each table row carries its own revision, so sorting a column no longer
diffs another commit.
- Drop the unscoped F5 shortcuts and remove the delegate from the key
handler when the tab is disposed.
- Reload the history when the filename changes, and refuse to diff rows
loaded for another file.
- Read the git history in a background thread and show a message in the tab
instead of only logging errors.
- Remove the comments that only restate the code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../org/apache/hop/git/BaseRevisionDelegate.java | 474 +++++++++++++++++++++
.../main/java/org/apache/hop/git/GitResource.java | 2 +
.../apache/hop/git/PipelineRevisionDelegate.java | 112 +++++
.../apache/hop/git/WorkflowRevisionDelegate.java | 112 +++++
.../hop/git/messages/messages_en_US.properties | 19 +
plugins/misc/git/src/main/resources/revision.svg | 4 +
6 files changed, 723 insertions(+)
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/BaseRevisionDelegate.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/BaseRevisionDelegate.java
new file mode 100644
index 0000000000..ad18294a6b
--- /dev/null
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/BaseRevisionDelegate.java
@@ -0,0 +1,474 @@
+/*
+ * 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 java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.base.AbstractMeta;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopFileException;
+import org.apache.hop.core.listeners.IFilenameChangedListener;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.git.config.GitConfigSingleton;
+import org.apache.hop.git.model.UIGit;
+import org.apache.hop.git.model.revision.ObjectRevision;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.FormDataBuilder;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
+import org.apache.hop.ui.core.gui.IToolbarContainer;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ColumnsResizer;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
+import org.apache.hop.ui.hopgui.ToolbarFacade;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TableItem;
+
+/** Base class sharing the revisions tab between the pipeline and the workflow
graphs. */
+public abstract class BaseRevisionDelegate {
+
+ public static final Class<?> PKG = BaseRevisionDelegate.class; // i18n
+
+ protected final HopGui hopGui;
+
+ private GuiToolbarWidgets toolBarWidgets;
+ private TableView wRevisions;
+
+ /** Shown instead of the table as long as there is no revision to list. */
+ private Label wMessage;
+
+ /**
+ * The file the rows were loaded for. Commit ids are repository-wide, so the
commits of another
+ * file must never be compared with the file being edited.
+ */
+ private String loadedFilename;
+
+ /** Identifies the last refresh, so a slow history walk never overwrites a
more recent one. */
+ private int refreshId;
+
+ protected BaseRevisionDelegate(HopGui hopGui) {
+ super();
+ this.hopGui = hopGui;
+ }
+
+ /** The toolbar root id declared by the subclass, used to create the toolbar
widgets. */
+ protected abstract String getToolbarParentId();
+
+ /** The pipeline or workflow being edited, its filename is null when it
isn't saved yet. */
+ protected abstract AbstractMeta getMeta();
+
+ /** Whether the pipeline or workflow being edited holds changes which aren't
written to disk. */
+ protected abstract boolean hasChanges();
+
+ /** The toolbar items which only make sense when a revision is selected in
the table. */
+ protected abstract List<String> getSelectionToolbarItemIds();
+
+ /**
+ * Opens the graphical comparison of two revisions of the file, as a
pipeline or as a workflow.
+ *
+ * @param relativePath the path of the file, relative to the root of the git
repository
+ * @param commitIdNew the identifier of the most recent version to compare
+ * @param commitIdOld the identifier of the oldest version to compare
+ * @throws HopException when the file can't be read or compared
+ */
+ protected abstract void showGraphDiff(String relativePath, String
commitIdNew, String commitIdOld)
+ throws HopException;
+
+ protected CTabItem createRevisionsTab(CTabFolder tabFolder) {
+ // There are no revisions to show
+ if (!GitConfigSingleton.getConfig().isEnabled()
+ || GitGuiPlugin.getInstance().getGit() == null) {
+ return null;
+ }
+
+ CTabItem tab = new CTabItem(tabFolder, SWT.NONE);
+ tab.setFont(GuiResource.getInstance().getFontDefault());
+ tab.setImage(GitResource.getInstance().getRevisionImage());
+ tab.setText(BaseMessages.getString(PKG, "Revisions.Tab.Name"));
+
+ Composite composite = new Composite(tabFolder, SWT.NONE);
+ tab.setControl(composite);
+ composite.setLayout(new FormLayout());
+
+ // Create toolbar
+ IToolbarContainer toolBarContainer =
+ ToolbarFacade.createToolbarContainer(composite, SWT.WRAP | SWT.LEFT |
SWT.HORIZONTAL);
+ toolBarWidgets = new GuiToolbarWidgets();
+ toolBarWidgets.registerGuiPluginObject(this);
+ toolBarWidgets.createToolbarWidgets(toolBarContainer,
getToolbarParentId());
+ Control toolBar = toolBarContainer.getControl();
+ toolBar.setLayoutData(new FormDataBuilder().fullWidth().top().result());
+ toolBar.pack();
+ PropsUi.setLook(toolBar, Props.WIDGET_STYLE_TOOLBAR);
+
+ // Create the table
+ ColumnInfo[] revisionColumns = {
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "Revisions.ColumnMessage.Label"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "Revisions.ColumnAuthor.Label"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "Revisions.ColumnDate.Label"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "Revisions.ColumnRevision.Label"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true),
+ };
+ wRevisions =
+ new TableView(
+ hopGui.getVariables(),
+ composite,
+ SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION,
+ revisionColumns,
+ 1,
+ null,
+ PropsUi.getInstance());
+ wRevisions.setReadonly(true);
+ wRevisions.setLayoutData(new FormDataBuilder().fullWidth().top(toolBar,
0).bottom().result());
+ wRevisions.getTable().addListener(SWT.Resize, new ColumnsResizer(4, 54,
18, 16, 8));
+ wRevisions.getTable().addListener(SWT.MouseDoubleClick, event ->
showTextDiff());
+ wRevisions.getTable().addListener(SWT.Selection, event ->
enableToolbarItems());
+ PropsUi.setLook(wRevisions);
+
+ // Takes the place of the table, only one of them is visible at a time
+ wMessage = new Label(composite, SWT.WRAP);
+ wMessage.setLayoutData(
+ new FormDataBuilder()
+ .left(0, PropsUi.getMargin())
+ .right(100, -PropsUi.getMargin())
+ .top(toolBar, PropsUi.getMargin())
+ .bottom()
+ .result());
+ PropsUi.setLook(wMessage);
+
+ // The listener is called before the new filename is set, and not always
in the UI thread
+ Display display = composite.getDisplay();
+ AbstractMeta meta = getMeta();
+ IFilenameChangedListener filenameListener =
+ (object, oldFilename, newFilename) -> {
+ if (!display.isDisposed()) {
+ display.asyncExec(this::refresh);
+ }
+ };
+ meta.addFilenameChangedListener(filenameListener);
+
+ // Using the toolbar registers this delegate in the key handler, which is
never told that the
+ // tab is gone and would keep the whole graph in memory after the file is
closed.
+ composite.addListener(
+ SWT.Dispose,
+ event -> {
+ meta.removeFilenameChangedListener(filenameListener);
+ HopGuiKeyHandler.getInstance().removeParentObjectToHandle(this);
+ });
+
+ this.refresh();
+
+ return tab;
+ }
+
+ /**
+ * Reloads the git history of the current file. The history is read in a
background thread, as
+ * this tab is created along with the execution results, which have to stay
responsive.
+ */
+ public void refresh() {
+ if (wRevisions == null || wRevisions.isDisposed()) {
+ return;
+ }
+
+ String filename = getMeta().getFilename();
+ int currentRefreshId = ++refreshId;
+ loadedFilename = filename;
+ wRevisions.removeAll();
+ enableToolbarItems();
+
+ if (filename == null) {
+ showMessage(BaseMessages.getString(PKG, "Revisions.Message.NotSaved"));
+ return;
+ }
+
+ UIGit git = GitGuiPlugin.getInstance().getGit();
+ if (git == null) {
+ showMessage(BaseMessages.getString(PKG,
"Revisions.Message.NoRepository"));
+ return;
+ }
+
+ String relativePath;
+ try {
+ relativePath = calculateRelativePath(git.getDirectory(), filename);
+ } catch (Exception e) {
+ LogChannel.UI.logError("Error locating file '" + filename + "' in git
repository", e);
+ showMessage(
+ BaseMessages.getString(
+ PKG, "Revisions.Message.Error", filename,
Const.NVL(e.getMessage(), e.toString())));
+ return;
+ }
+ if (relativePath == null) {
+ showMessage(
+ BaseMessages.getString(
+ PKG, "Revisions.Message.OutsideRepository", filename,
git.getDirectory()));
+ return;
+ }
+
+ showMessage(BaseMessages.getString(PKG, "Revisions.Message.Loading"));
+
+ Display display = wRevisions.getDisplay();
+ Thread thread =
+ new Thread(
+ () -> {
+ List<ObjectRevision> fileRevisions = new ArrayList<>();
+ Exception error = null;
+ try {
+ fileRevisions = git.getRevisions(relativePath);
+ } catch (Exception e) {
+ error = e;
+ }
+ List<ObjectRevision> loadedRevisions = fileRevisions;
+ Exception loadError = error;
+ if (!display.isDisposed()) {
+ display.asyncExec(
+ () -> showRevisions(currentRefreshId, git,
loadedRevisions, loadError));
+ }
+ },
+ "Git revisions of " + relativePath);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ private void showRevisions(
+ int loadedRefreshId, UIGit git, List<ObjectRevision> fileRevisions,
Exception error) {
+ // The tab can be closed, or the file renamed, while the history was being
read
+ if (wRevisions.isDisposed() || loadedRefreshId != refreshId) {
+ return;
+ }
+
+ if (error != null) {
+ LogChannel.UI.logError("Error getting git revisions of file '" +
loadedFilename + "'", error);
+ showMessage(
+ BaseMessages.getString(
+ PKG,
+ "Revisions.Message.Error",
+ loadedFilename,
+ Const.NVL(error.getMessage(), error.toString())));
+ return;
+ }
+
+ int count = 0;
+ wRevisions.setRedraw(false);
+ try {
+ wRevisions.removeAll();
+ for (ObjectRevision revision : fileRevisions) {
+ if (UIGit.WORKINGTREE.equals(revision.getRevisionId())) {
+ continue;
+ }
+
+ TableItem item = new TableItem(wRevisions.table, SWT.NONE);
+ // The row order changes when a column is sorted, and the commit id is
shortened in the
+ // table: the row has to carry its own revision, which TableView keeps
with it on a sort.
+ item.setData(revision);
+ item.setText(1, Const.NVL(revision.getComment(), ""));
+ item.setText(2, Const.NVL(revision.getLogin(), ""));
+ item.setText(3, getDateString(revision.getCreationDate()));
+ item.setText(4, git.getShortenedName(revision.getRevisionId()));
+ count++;
+ }
+ wRevisions.optimizeTableView();
+
+ // Preselect the most recent revision so the diff actions always have a
subject
+ if (count > 0) {
+ wRevisions.table.setSelection(0);
+ }
+ } finally {
+ // Always restore the redraw, otherwise the table stays frozen after an
error
+ wRevisions.setRedraw(true);
+ }
+
+ if (count > 0) {
+ wMessage.setVisible(false);
+ wRevisions.setVisible(true);
+ } else {
+ showMessage(BaseMessages.getString(PKG,
"Revisions.Message.NoRevisions"));
+ }
+
+ // A programmatic selection doesn't fire an event, update the actions
ourselves
+ enableToolbarItems();
+ }
+
+ private void showMessage(String message) {
+ wMessage.setText(message);
+ wRevisions.setVisible(false);
+ wMessage.setVisible(true);
+ }
+
+ /** The working tree side of the comparison stays editable, like in the git
perspective. */
+ public void showTextDiff() {
+ try {
+ RevisionDiff diff = getSelectedRevisionDiff();
+ if (diff != null) {
+ warnAboutUnsavedChanges();
+ GitGuiPlugin.getInstance()
+ .showTextFileDiff(diff.relativePath(), diff.commitIdNew(),
diff.commitIdOld());
+ }
+ } catch (Exception e) {
+ showDiffError("Revisions.ShowTextDiff.Error.Message",
getMeta().getFilename(), e);
+ }
+ }
+
+ public void showVisualDiff() {
+ try {
+ RevisionDiff diff = getSelectedRevisionDiff();
+ if (diff != null) {
+ warnAboutUnsavedChanges();
+ showGraphDiff(diff.relativePath(), diff.commitIdNew(),
diff.commitIdOld());
+ }
+ } catch (Exception e) {
+ showDiffError("Revisions.ShowVisualDiff.Error.Message",
getMeta().getFilename(), e);
+ }
+ }
+
+ /**
+ * Determines what has to be compared, the selected revision with the file
currently being edited,
+ * as it is stored in the working tree.
+ *
+ * @return the comparison to show, null when there is nothing to compare
+ * @throws HopException when the path of the file can't be resolved in the
git repository
+ */
+ private RevisionDiff getSelectedRevisionDiff() throws HopException {
+ String filename = getMeta().getFilename();
+ UIGit git = GitGuiPlugin.getInstance().getGit();
+ if (filename == null || git == null) {
+ return null;
+ }
+
+ // The rows still list the commits of the previous file, reload instead of
comparing them
+ if (!filename.equals(loadedFilename)) {
+ refresh();
+ return null;
+ }
+
+ ObjectRevision revision = getSelectedRevision();
+ if (revision == null) {
+ return null;
+ }
+
+ try {
+ String relativePath = calculateRelativePath(git.getDirectory(),
filename);
+ if (relativePath == null) {
+ return null;
+ }
+ return new RevisionDiff(relativePath, UIGit.WORKINGTREE,
revision.getRevisionId());
+ } catch (HopFileException | FileSystemException e) {
+ throw new HopException(
+ "Unable to locate file '" + filename + "' in git repository " +
git.getDirectory(), e);
+ }
+ }
+
+ private void enableToolbarItems() {
+ boolean selected = getSelectedRevision() != null;
+ for (String itemId : getSelectionToolbarItemIds()) {
+ toolBarWidgets.enableToolbarItem(itemId, selected);
+ }
+ }
+
+ private ObjectRevision getSelectedRevision() {
+ if (wRevisions == null || wRevisions.isDisposed()) {
+ return null;
+ }
+ TableItem[] selection = wRevisions.table.getSelection();
+ if (selection.length == 1 && selection[0].getData() instanceof
ObjectRevision revision) {
+ return revision;
+ }
+ return null;
+ }
+
+ /**
+ * Warns that the comparison is made against the last saved version of the
file, as long as the
+ * editor still holds changes which aren't written to disk yet.
+ */
+ private void warnAboutUnsavedChanges() {
+ if (!hasChanges()) {
+ return;
+ }
+ MessageBox box = new MessageBox(hopGui.getShell(), SWT.OK |
SWT.ICON_WARNING);
+ box.setText(BaseMessages.getString(PKG,
"Revisions.UnsavedChanges.Warning.Title"));
+ box.setMessage(BaseMessages.getString(PKG,
"Revisions.UnsavedChanges.Warning.Message"));
+ box.open();
+ }
+
+ private void showDiffError(String messageKey, String fileName, Exception e) {
+ new ErrorDialog(
+ hopGui.getShell(),
+ BaseMessages.getString(PKG, "Revisions.Error.Title"),
+ BaseMessages.getString(PKG, messageKey, fileName),
+ e);
+ }
+
+ /**
+ * @return the path git knows the file by, null when the file is outside of
the repository
+ */
+ private String calculateRelativePath(String rootFolder, String filename)
+ throws HopFileException, FileSystemException {
+ FileObject root = HopVfs.getFileObject(rootFolder);
+ FileObject file = HopVfs.getFileObject(filename);
+ if (!root.getName().isDescendent(file.getName())) {
+ return null;
+ }
+ return root.getName().getRelativeName(file.getName());
+ }
+
+ private String getDateString(Date date) {
+ return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(date);
+ }
+
+ /**
+ * The git coordinates of a comparison between the file being edited and a
selected revision.
+ *
+ * @param relativePath the path of the file, relative to the root of the git
repository
+ * @param commitIdNew the working tree, holding the file currently being
edited
+ * @param commitIdOld the identifier of the selected revision
+ */
+ protected record RevisionDiff(String relativePath, String commitIdNew,
String commitIdOld) {}
+}
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
index d536611040..902f4927b5 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
@@ -64,6 +64,7 @@ public class GitResource {
@Getter private final Image restoreImage;
@Getter private final Image resetImage;
@Getter private final Image tagImage;
+ @Getter private final Image revisionImage;
/** Utility class */
GitResource() {
@@ -114,6 +115,7 @@ public class GitResource {
remoteImage = getImage("remote.svg");
restoreImage = getImage("git-restore.svg");
resetImage = getImage("git-reset.svg");
+ revisionImage = getImage("revision.svg");
tagImage = getImage("tag.svg");
}
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/PipelineRevisionDelegate.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/PipelineRevisionDelegate.java
new file mode 100644
index 0000000000..8b324568d0
--- /dev/null
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/PipelineRevisionDelegate.java
@@ -0,0 +1,112 @@
+/*
+ * 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 java.util.List;
+import org.apache.hop.base.AbstractMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.tab.GuiTab;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+
+@GuiPlugin(name = "Pipeline revisions", description = "Revisions of the
pipeline")
+public class PipelineRevisionDelegate extends BaseRevisionDelegate {
+
+ public static final String GUI_PLUGIN_TOOLBAR_PARENT_ID =
"PipelineRevisions-Toolbar";
+ public static final String TOOLBAR_ITEM_REFRESH =
"PipelineRevisions-Toolbar-10100-Refresh";
+ public static final String TOOLBAR_ITEM_SHOW_TEXT_DIFF =
+ "PipelineRevisions-Toolbar-10200-ShowTextDiff";
+ public static final String TOOLBAR_ITEM_SHOW_VISUAL_DIFF =
+ "PipelineRevisions-Toolbar-10210-ShowVisualDiff";
+
+ private final HopGuiPipelineGraph pipelineGraph;
+
+ public PipelineRevisionDelegate(HopGui hopGui, HopGuiPipelineGraph
pipelineGraph) {
+ super(hopGui);
+ this.pipelineGraph = pipelineGraph;
+ }
+
+ @Override
+ protected String getToolbarParentId() {
+ return GUI_PLUGIN_TOOLBAR_PARENT_ID;
+ }
+
+ @Override
+ protected AbstractMeta getMeta() {
+ return pipelineGraph.getPipelineMeta();
+ }
+
+ @Override
+ protected boolean hasChanges() {
+ return pipelineGraph.hasChanged();
+ }
+
+ @Override
+ protected List<String> getSelectionToolbarItemIds() {
+ return List.of(TOOLBAR_ITEM_SHOW_TEXT_DIFF, TOOLBAR_ITEM_SHOW_VISUAL_DIFF);
+ }
+
+ @GuiTab(
+ id = "90000-pipeline-revisions-tab",
+ parentId = HopGuiPipelineGraph.PIPELINE_GRAPH_TABS,
+ description = "Pipeline revisions")
+ public CTabItem createRevisionsTab(CTabFolder tabFolder) {
+ return super.createRevisionsTab(tabFolder);
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_REFRESH,
+ toolTip = "i18n::System.Button.Refresh",
+ image = "ui/images/refresh.svg")
+ public void refresh() {
+ super.refresh();
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_SHOW_TEXT_DIFF,
+ toolTip = "i18n::Revisions.Toolbar.ShowTextDiff.Tooltip",
+ image = "diff-text.svg",
+ separator = true)
+ public void showTextDiff() {
+ super.showTextDiff();
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_SHOW_VISUAL_DIFF,
+ toolTip = "i18n::Revisions.Toolbar.ShowVisualDiff.Tooltip",
+ image = "diff-graph.svg")
+ public void showVisualDiff() {
+ super.showVisualDiff();
+ }
+
+ @Override
+ protected void showGraphDiff(String relativePath, String commitIdNew, String
commitIdOld)
+ throws HopException {
+ GitGuiPlugin.getInstance().showPipelineFileDiff(relativePath, commitIdNew,
commitIdOld);
+ }
+}
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/WorkflowRevisionDelegate.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/WorkflowRevisionDelegate.java
new file mode 100644
index 0000000000..0ddebe37ff
--- /dev/null
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/WorkflowRevisionDelegate.java
@@ -0,0 +1,112 @@
+/*
+ * 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 java.util.List;
+import org.apache.hop.base.AbstractMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.tab.GuiTab;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+
+@GuiPlugin(name = "Workflow revisions", description = "Revisions of the
workflow")
+public class WorkflowRevisionDelegate extends BaseRevisionDelegate {
+
+ public static final String GUI_PLUGIN_TOOLBAR_PARENT_ID =
"WorkflowRevisions-Toolbar";
+ public static final String TOOLBAR_ITEM_REFRESH =
"WorkflowRevisions-Toolbar-10100-Refresh";
+ public static final String TOOLBAR_ITEM_SHOW_TEXT_DIFF =
+ "WorkflowRevisions-Toolbar-10200-ShowTextDiff";
+ public static final String TOOLBAR_ITEM_SHOW_VISUAL_DIFF =
+ "WorkflowRevisions-Toolbar-10210-ShowVisualDiff";
+
+ private final HopGuiWorkflowGraph workflowGraph;
+
+ public WorkflowRevisionDelegate(HopGui hopGui, HopGuiWorkflowGraph
workflowGraph) {
+ super(hopGui);
+ this.workflowGraph = workflowGraph;
+ }
+
+ @Override
+ protected String getToolbarParentId() {
+ return GUI_PLUGIN_TOOLBAR_PARENT_ID;
+ }
+
+ @Override
+ protected AbstractMeta getMeta() {
+ return workflowGraph.getWorkflowMeta();
+ }
+
+ @Override
+ protected boolean hasChanges() {
+ return workflowGraph.hasChanged();
+ }
+
+ @Override
+ protected List<String> getSelectionToolbarItemIds() {
+ return List.of(TOOLBAR_ITEM_SHOW_TEXT_DIFF, TOOLBAR_ITEM_SHOW_VISUAL_DIFF);
+ }
+
+ @GuiTab(
+ id = "90000-workflow-revisions-tab",
+ parentId = HopGuiWorkflowGraph.WORKFLOW_GRAPH_TABS,
+ description = "Workflow revisions")
+ public CTabItem createRevisionsTab(CTabFolder tabFolder) {
+ return super.createRevisionsTab(tabFolder);
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_REFRESH,
+ toolTip = "i18n::System.Button.Refresh",
+ image = "ui/images/refresh.svg")
+ public void refresh() {
+ super.refresh();
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_SHOW_TEXT_DIFF,
+ toolTip = "i18n::Revisions.Toolbar.ShowTextDiff.Tooltip",
+ image = "diff-text.svg",
+ separator = true)
+ public void showTextDiff() {
+ super.showTextDiff();
+ }
+
+ @Override
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_SHOW_VISUAL_DIFF,
+ toolTip = "i18n::Revisions.Toolbar.ShowVisualDiff.Tooltip",
+ image = "diff-graph.svg")
+ public void showVisualDiff() {
+ super.showVisualDiff();
+ }
+
+ @Override
+ protected void showGraphDiff(String relativePath, String commitIdNew, String
commitIdOld)
+ throws HopException {
+ GitGuiPlugin.getInstance().showWorkflowFileDiff(relativePath, commitIdNew,
commitIdOld);
+ }
+}
diff --git
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
index 72fc90b1a0..e54a016bcd 100644
---
a/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
+++
b/plugins/misc/git/src/main/resources/org/apache/hop/git/messages/messages_en_US.properties
@@ -220,3 +220,22 @@ GitGuiPlugin.Dialog.CommitRefused.Message=The commit was
refused\:\n\n{0}
GitGuiPlugin.CommitRefused.NoReason=No reason given.
GitCommitPerspective.Error.CommitRefused.Message=Commit refused\: {0}
GitCommitPerspective.CommitRefused.NoReason=no reason given
+
+Revisions.ColumnAuthor.Label=Author
+Revisions.ColumnDate.Label=Date
+Revisions.ColumnMessage.Label=Message
+Revisions.ColumnRevision.Label=Commit
+Revisions.Error.Title=Error
+Revisions.Message.Error=Unable to get the revisions of file {0} : {1}
+Revisions.Message.Loading=Loading the revisions...
+Revisions.Message.NoRepository=This project is not a git repository.
+Revisions.Message.NoRevisions=This file has no revision yet.
+Revisions.Message.NotSaved=Save the file to see its revisions.
+Revisions.Message.OutsideRepository=The file {0} is not in the git repository
{1}.
+Revisions.ShowTextDiff.Error.Message=Error while doing text diff on file : {0}
+Revisions.ShowVisualDiff.Error.Message=Error while doing visual diff on file :
{0}
+Revisions.Tab.Name=Revisions
+Revisions.Toolbar.ShowTextDiff.Tooltip=Show text diff
+Revisions.Toolbar.ShowVisualDiff.Tooltip=Show visual diff
+Revisions.UnsavedChanges.Warning.Message=Save the file first : the comparison
is made with the last saved version, your pending changes are not taken into
account.
+Revisions.UnsavedChanges.Warning.Title=Unsaved changes
\ No newline at end of file
diff --git a/plugins/misc/git/src/main/resources/revision.svg
b/plugins/misc/git/src/main/resources/revision.svg
new file mode 100644
index 0000000000..f071561cc1
--- /dev/null
+++ b/plugins/misc/git/src/main/resources/revision.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#0E3A5A" d="M10 22a2 2 0 01-2-2V4a2 2 0 012-2h11a2 2 0 012
2v16a2 2 0 01-2 2H10zm-.5-2a.5.5 0 00.5.5h11a.5.5 0 00.5-.5V4a.5.5 0
00-.5-.5H10a.5.5 0 00-.5.5v16zM6.17 4.165a.75.75 0 01-.335
1.006c-.228.114-.295.177-.315.201a.037.037 0 00-.008.016.387.387 0
00-.012.112v13c0 .07.008.102.012.112a.03.03 0
00.008.016c.02.024.087.087.315.201a.75.75 0 11-.67
1.342c-.272-.136-.58-.315-.81-.598C4.1 19.259 4 18.893 4
18.5v-13c0-.393.1-.759.355-1.073.23-.283.538-.462.81-.598a.75.75 0 [...]
+</svg>
\ No newline at end of file