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

hansva 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 2aac58b9c9 allow to push git tags to remote. fixes #7880 (#7888)
2aac58b9c9 is described below

commit 2aac58b9c91e893e78935c5082f3b53e958fe74a
Author: Bart Maertens <[email protected]>
AuthorDate: Wed Aug 12 16:34:51 2026 +0200

    allow to push git tags to remote. fixes #7880 (#7888)
---
 .../java/org/apache/hop/git/GitPerspective.java    | 68 ++++++++++++++++++----
 .../main/java/org/apache/hop/git/model/UIGit.java  | 46 +++++++++++----
 .../hop/git/messages/messages_en_US.properties     |  2 +
 .../java/org/apache/hop/git/model/UIGitTest.java   | 31 ++++++++++
 4 files changed, 127 insertions(+), 20 deletions(-)

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 1aeca3fa05..66699922be 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
@@ -41,6 +41,7 @@ import org.apache.hop.git.config.GitConfigSingleton;
 import org.apache.hop.git.info.DiffStyledTextComp;
 import org.apache.hop.git.model.UIFile;
 import org.apache.hop.git.model.UIGit;
+import org.apache.hop.git.model.VCS;
 import org.apache.hop.git.util.FileTypeUtils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.FormDataBuilder;
@@ -392,10 +393,11 @@ public class GitPerspective implements IHopPerspective {
           boolean isTags = ref.getName().startsWith(Constants.R_TAGS);
 
           setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_CHECKOUT, 
!isCurrentBranch);
-          setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_PUSH, isHeads);
+          setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_PUSH, isHeads || 
isTags);
           setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_PULL, isHeads);
           setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_RENAME, isHeads);
-          setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_DELETE, 
!isCurrentBranch);
+          setMenuItemEnabled(
+              refMenuWidgets, REF_CONTEXT_MENU_DELETE, !isCurrentBranch && 
(isHeads || isTags));
 
           MenuItem menuItem = 
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_CREATE_BRANCH);
           if (menuItem != null) {
@@ -1159,12 +1161,6 @@ public class GitPerspective implements IHopPerspective {
     }
   }
 
-  @GuiMenuElement(
-      root = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
-      parentId = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
-      id = REF_CONTEXT_MENU_PUSH,
-      label = "i18n::GitPerspective.Menu.Push.Text",
-      image = "push.svg")
   @GuiToolbarElement(
       root = GitPerspective.GUI_PLUGIN_HISTORY_TOOLBAR_PARENT_ID,
       id = GitPerspective.TOOLBAR_ITEM_PUSH,
@@ -1185,6 +1181,34 @@ public class GitPerspective implements IHopPerspective {
     }
   }
 
+  /** Push the selected branch or tag in the references tree. */
+  @GuiMenuElement(
+      root = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
+      parentId = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
+      id = REF_CONTEXT_MENU_PUSH,
+      label = "i18n::GitPerspective.Menu.Push.Text",
+      image = "push.svg")
+  public void pushReference() {
+    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()));
+
+      // Refresh refs and commit history
+      refresh(false);
+    } catch (Exception e) {
+      new ErrorDialog(
+          getShell(),
+          BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.PushError.Header"),
+          BaseMessages.getString(PKG, "GitGuiPlugin.Dialog.PushError.Message"),
+          e);
+    }
+  }
+
   @GuiMenuElement(
       root = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
       parentId = GUI_PLUGIN_REF_CONTEXT_MENU_PARENT_ID,
@@ -1252,7 +1276,7 @@ public class GitPerspective implements IHopPerspective {
     if (ref != null) {
       if (ref.getName().startsWith(Constants.R_HEADS)) {
         deleteBranch(ref);
-      } else {
+      } else if (ref.getName().startsWith(Constants.R_TAGS)) {
         deleteTag(ref);
       }
     }
@@ -1297,7 +1321,9 @@ public class GitPerspective implements IHopPerspective {
               PKG, "GitGuiPlugin.Dialog.Tag.DeleteTagConfirmation.Message", 
name));
 
       if (dialog.open() == SWT.YES) {
-        git.deleteTag(name);
+        if (git.deleteTag(name) && git.hasRemote()) {
+          deleteRemoteTag(git, name);
+        }
 
         // Refresh refs and commit history
         refresh(false);
@@ -1305,6 +1331,28 @@ public class GitPerspective implements IHopPerspective {
     }
   }
 
+  /** Ask to delete the tag on the remote as well, deleting it locally only 
leaves it there. */
+  private void deleteRemoteTag(UIGit git, String name) {
+    MessageBox dialog = new MessageBox(getShell(), SWT.ICON_QUESTION | SWT.YES 
| SWT.NO);
+    dialog.setText(
+        BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.Tag.DeleteRemoteTagConfirmation.Header"));
+    dialog.setMessage(
+        BaseMessages.getString(
+            PKG, 
"GitGuiPlugin.Dialog.Tag.DeleteRemoteTagConfirmation.Message", name));
+
+    if (dialog.open() == SWT.YES) {
+      try {
+        git.deleteRemoteTag(name);
+      } catch (Exception e) {
+        new ErrorDialog(
+            getShell(),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Header"),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Message"),
+            e);
+      }
+    }
+  }
+
   /**
    * Sets the diff text in the appropriate widget (colored styled text for 
desktop, plain text for
    * web).
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
index 1423571820..8c89c46f07 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
@@ -656,6 +656,7 @@ public class UIGit extends VCS {
     return push("default");
   }
 
+  /** Ask which branch or tag to push and push it. */
   public boolean push(String type) throws HopException {
     if (!hasRemote()) {
       throw new HopException("There is no remote set up to push to. Please set 
this up.");
@@ -684,19 +685,38 @@ public class UIGit extends VCS {
         return false;
       }
     }
-    try {
-      name = name == null ? null : getExpandedName(name, type);
+    return push(type, name);
+  }
+
+  /**
+   * Push a single branch or tag by name, without asking which one.
+   *
+   * @param type the type of the reference: {@link VCS#TYPE_BRANCH} or {@link 
VCS#TYPE_TAG}
+   * @param name the short name of the branch or tag, null to push the default 
refspec
+   */
+  public boolean push(String type, String name) throws HopException {
+    return push(name == null ? null : new RefSpec(getExpandedName(name, 
type)));
+  }
 
-      PushCommand cmd;
+  /** Delete a tag on the remote: git push origin :refs/tags/name */
+  public boolean deleteRemoteTag(String name) throws HopException {
+    return push(new RefSpec(":" + getExpandedName(name, VCS.TYPE_TAG)));
+  }
+
+  private boolean push(RefSpec refSpec) throws HopException {
+    if (!hasRemote()) {
+      throw new HopException("There is no remote set up to push to. Please set 
this up.");
+    }
+    try {
+      PushCommand cmd = git.push();
 
       String url = git.getRepository().getConfig().getString("remote", 
"origin", "url");
-      cmd = git.push();
       if (!StringUtils.isEmpty(url) && (url.startsWith("https://";) || 
url.startsWith("http://";))) {
         cmd.setCredentialsProvider(credentialsProvider);
       }
 
-      if (name != null) {
-        cmd.setRefSpecs(new RefSpec(name));
+      if (refSpec != null) {
+        cmd.setRefSpecs(refSpec);
       }
 
       Iterable<PushResult> resultIterable = cmd.call();
@@ -709,7 +729,7 @@ public class UIGit extends VCS {
           || e.getMessage()
               .contains(CONST_NOT_AUTHORIZED)) { // when the cached credential 
does not work
         if (promptUsernamePassword()) {
-          return push(type);
+          return push(refSpec);
         }
       } else {
         throw new HopException("There was an error doing a git push", e);
@@ -727,23 +747,29 @@ public class UIGit extends VCS {
           result.getRemoteUpdates().stream()
               .filter(update -> update.getStatus() != 
RemoteRefUpdate.Status.OK)
               .filter(update -> update.getStatus() != 
RemoteRefUpdate.Status.UP_TO_DATE)
+              // Deleting a ref that is already gone on the remote is not an 
error
+              .filter(update -> update.getStatus() != 
RemoteRefUpdate.Status.NON_EXISTING)
               .forEach(
                   // for each failed refspec
                   update -> {
+                    boolean isTag = 
update.getRemoteName().startsWith(Constants.R_TAGS);
                     sb.append("Errors while pushing: ")
                         .append("\n")
                         .append("Destination: ")
                         .append(result.getURI().toString())
                         .append("\n")
-                        .append("Branch name: ")
-                        .append(update.getSrcRef())
+                        .append(isTag ? "Tag name: " : "Branch name: ")
+                        // A delete has no source ref, report the ref on the 
remote instead
+                        
.append(Repository.shortenRefName(update.getRemoteName()))
                         .append("\n");
                     switch (update.getStatus()) {
                       case REJECTED_NONFASTFORWARD:
                         sb.append(" * ")
                             .append(update.getStatus().toString())
                             .append(
-                                " - Remote repository contains changes. Merge 
the remote changes (e.g. 'git pull') before pushing again.")
+                                isTag
+                                    ? " - The tag already exists on the remote 
and points to another commit."
+                                    : " - Remote repository contains changes. 
Merge the remote changes (e.g. 'git pull') before pushing again.")
                             .append("\n");
                         break;
                       default:
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 e957e2b51f..efb5513ab2 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
@@ -84,6 +84,8 @@ GitGuiPlugin.Dialog.UnstageError.Header=GIT Unstage Error
 GitGuiPlugin.Dialog.UnstageError.Message=There was an error unstaging the 
selected files
 GitGuiPlugin.Dialog.Tag.CreateTag.Header=Create a Tag
 GitGuiPlugin.Dialog.Tag.CreateTag.Message=Enter the name of the new tag
+GitGuiPlugin.Dialog.Tag.DeleteRemoteTagConfirmation.Header=Delete a Tag
+GitGuiPlugin.Dialog.Tag.DeleteRemoteTagConfirmation.Message=Also delete tag 
''{0}'' on the remote?
 GitGuiPlugin.Dialog.Tag.DeleteTagConfirmation.Header=Delete a Tag
 GitGuiPlugin.Dialog.Tag.DeleteTagConfirmation.Message=Are you sure you want to 
delete tag ''{0}'' ?
 GitGuiPlugin.Dialog.SelectFilesToCommit.Header=Commit Message
diff --git 
a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java 
b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
index b500bb1e57..c8ddc132e0 100644
--- a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
+++ b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
@@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
@@ -358,6 +359,36 @@ public class UIGitTest extends RepositoryTestCase {
     assertEquals("refs/remotes/origin/master", 
uiGit.getExpandedName("origin/master", "branch"));
   }
 
+  @Test
+  public void testPushAndDeleteTagByName() throws Exception {
+    // Set remote
+    Git git2 = new Git(db2);
+    UIGit uiGit2 = new UIGit();
+    uiGit2.setGit(git2);
+    setupRemote();
+
+    git.commit().setMessage("initial commit").call();
+    git.tag().setName("v1").call();
+
+    // Push the tag itself, a default push doesn't include tags
+    assertTrue(uiGit.push());
+    assertFalse(uiGit2.getTags().contains("v1"));
+
+    assertTrue(uiGit.push(VCS.TYPE_TAG, "v1"));
+    assertTrue(uiGit2.getTags().contains("v1"));
+
+    // Delete the tag on the remote
+    assertTrue(uiGit.deleteRemoteTag("v1"));
+    assertFalse(uiGit2.getTags().contains("v1"));
+
+    // Deleting a tag that is already gone on the remote is not an error
+    assertTrue(uiGit.deleteRemoteTag("v1"));
+
+    // The name is known, so no selection dialog is opened
+    verify(uiGit, never()).getEnterSelectionDialog(any(), anyString(), 
anyString());
+    git2.close();
+  }
+
   @Test
   public void testShouldPushOnlyToOrigin() throws Exception {
     // origin for db2

Reply via email to