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