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 af08453b6e allow remote branch delete and rename in git perspective. 
fixes #7996 (#7998)
af08453b6e is described below

commit af08453b6ec58a8660bf973052f1b4827c3901c3
Author: Bart Maertens <[email protected]>
AuthorDate: Wed Aug 19 15:53:17 2026 +0200

    allow remote branch delete and rename in git perspective. fixes #7996 
(#7998)
---
 .../java/org/apache/hop/git/GitPerspective.java    | 141 ++++++++++-
 .../main/java/org/apache/hop/git/model/UIGit.java  | 260 ++++++++++++++++-----
 .../hop/git/messages/messages_en_US.properties     |   6 +
 .../java/org/apache/hop/git/model/UIGitTest.java   |  81 +++++++
 4 files changed, 431 insertions(+), 57 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 51de81518c..d182246341 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
@@ -392,12 +392,24 @@ public class GitPerspective implements IHopPerspective {
           boolean isRemotes = ref.getName().startsWith(Constants.R_REMOTES);
           boolean isTags = ref.getName().startsWith(Constants.R_TAGS);
 
+          // The default branch of the remote is off limits, renaming or 
deleting it breaks the
+          // repository for everyone. The remote is the one enforcing this, we 
only keep the
+          // obvious mistake out of reach.
+          //
+          boolean isProtectedRemote =
+              isRemotes && 
GitGuiPlugin.getInstance().getGit().isRemoteHead(ref.getName());
+
           setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_CHECKOUT, 
!isCurrentBranch);
           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 && 
(isHeads || isTags));
+              refMenuWidgets,
+              REF_CONTEXT_MENU_RENAME,
+              isHeads || (isRemotes && !isProtectedRemote));
+          setMenuItemEnabled(
+              refMenuWidgets,
+              REF_CONTEXT_MENU_DELETE,
+              !isCurrentBranch && (isHeads || isTags || (isRemotes && 
!isProtectedRemote)));
 
           MenuItem menuItem = 
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_CREATE_BRANCH);
           if (menuItem != null) {
@@ -1221,6 +1233,15 @@ public class GitPerspective implements IHopPerspective {
   public void renameReference() {
     Ref ref = this.getSelectedReference();
     if (ref != null) {
+      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())) {
+        return;
+      }
+
       UIGit git = GitGuiPlugin.getInstance().getGit();
       String oldName = git.getShortenedName(ref.getName());
 
@@ -1235,7 +1256,9 @@ public class GitPerspective implements IHopPerspective {
               case SWT.CR, SWT.KEYPAD_CR:
                 String newName = text.getText().trim();
                 if (!Utils.isEmpty(newName) && !newName.equals(oldName)) {
-                  if (git.renameBranch(oldName, newName)) {
+                  if (isRemote) {
+                    renameRemoteBranch(ref, oldName, newName);
+                  } else if (git.renameBranch(oldName, newName)) {
 
                     // If we rename the active branch
                     if (newName.equals(git.getBranch())) {
@@ -1276,6 +1299,8 @@ public class GitPerspective implements IHopPerspective {
     if (ref != null) {
       if (ref.getName().startsWith(Constants.R_HEADS)) {
         deleteBranch(ref);
+      } else if (ref.getName().startsWith(Constants.R_REMOTES)) {
+        deleteRemoteBranch(ref);
       } else if (ref.getName().startsWith(Constants.R_TAGS)) {
         deleteTag(ref);
       }
@@ -1308,6 +1333,116 @@ public class GitPerspective implements IHopPerspective {
     }
   }
 
+  /**
+   * Delete a branch on the remote. The branch is gone for everyone once this 
is pushed, so ask
+   * first and name the remote in the question.
+   */
+  protected void deleteRemoteBranch(Ref ref) {
+    if (ref == null) {
+      return;
+    }
+    UIGit git = GitGuiPlugin.getInstance().getGit();
+    String name = git.getShortenedName(ref.getName());
+
+    // The default branch of the remote is protected against an accidental 
delete
+    if (git.isRemoteHead(ref.getName())) {
+      return;
+    }
+
+    MessageBox dialog = new MessageBox(getShell(), SWT.ICON_WARNING | SWT.YES 
| SWT.NO);
+    dialog.setText(
+        BaseMessages.getString(
+            PKG, 
"GitGuiPlugin.Dialog.Branch.DeleteRemoteBranchConfirmation.Header"));
+    dialog.setMessage(
+        BaseMessages.getString(
+            PKG,
+            
"GitGuiPlugin.Dialog.Branch.DeleteRemoteBranchConfirmation.Message",
+            getBranchOnRemote(name),
+            getRemoteName(name)));
+
+    if (dialog.open() == SWT.YES) {
+      try {
+        git.deleteRemoteBranch(ref.getName());
+      } catch (Exception e) {
+        new ErrorDialog(
+            getShell(),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Header"),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Message"),
+            e);
+      }
+
+      // Refresh refs and commit history
+      refresh(false);
+    }
+  }
+
+  /**
+   * Rename a branch on the remote. Git can't rename over the wire: the branch 
is pushed under its
+   * new name and the old one is deleted, so ask before doing it.
+   *
+   * @param ref the remote tracking ref of the branch
+   * @param oldName the current name including the remote, e.g. origin/feature
+   * @param newName the new name as typed by the user, with or without the 
remote prefix
+   */
+  private void renameRemoteBranch(Ref ref, String oldName, String newName) {
+    UIGit git = GitGuiPlugin.getInstance().getGit();
+
+    // 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.
+    //
+    String remote = getRemoteName(oldName);
+    String newBranchName = newName.startsWith(remote + "/") ? 
getBranchOnRemote(newName) : newName;
+
+    if (Utils.isEmpty(newBranchName)
+        || !Repository.isValidRefName(Constants.R_HEADS + newBranchName)) {
+      MessageBox box = new MessageBox(getShell(), SWT.ICON_ERROR | SWT.OK);
+      box.setText(BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.Branch.InvalidName.Header"));
+      box.setMessage(
+          BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.Branch.InvalidName.Message", newName));
+      box.open();
+      return;
+    }
+
+    MessageBox dialog = new MessageBox(getShell(), SWT.ICON_WARNING | SWT.YES 
| SWT.NO);
+    dialog.setText(
+        BaseMessages.getString(
+            PKG, 
"GitGuiPlugin.Dialog.Branch.RenameRemoteBranchConfirmation.Header"));
+    dialog.setMessage(
+        BaseMessages.getString(
+            PKG,
+            
"GitGuiPlugin.Dialog.Branch.RenameRemoteBranchConfirmation.Message",
+            getBranchOnRemote(oldName),
+            newBranchName,
+            remote));
+
+    if (dialog.open() == SWT.YES) {
+      try {
+        git.renameRemoteBranch(ref.getName(), newBranchName);
+      } catch (Exception e) {
+        new ErrorDialog(
+            getShell(),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Header"),
+            BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.PushError.Message"),
+            e);
+      }
+
+      // Refresh refs and commit history
+      refresh(false);
+    }
+  }
+
+  /** The remote in a shortened remote branch name: origin/feature/hop gives 
origin. */
+  private static String getRemoteName(String shortenedName) {
+    int slashIndex = shortenedName.indexOf('/');
+    return slashIndex < 0 ? shortenedName : shortenedName.substring(0, 
slashIndex);
+  }
+
+  /** The branch name in a shortened remote branch name: origin/feature/hop 
gives feature/hop. */
+  private static String getBranchOnRemote(String shortenedName) {
+    int slashIndex = shortenedName.indexOf('/');
+    return slashIndex < 0 ? shortenedName : shortenedName.substring(slashIndex 
+ 1);
+  }
+
   protected void deleteTag(Ref ref) {
     if (ref != null) {
       UIGit git = GitGuiPlugin.getInstance().getGit();
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 6bc41c80af..728b6d28e9 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
@@ -85,6 +85,7 @@ import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.StoredConfig;
@@ -706,25 +707,167 @@ public class UIGit extends VCS {
     return push(new RefSpec(":" + getExpandedName(name, VCS.TYPE_TAG)));
   }
 
+  /**
+   * Delete a branch on the remote: git push origin :refs/heads/name
+   *
+   * <p>The remote tracking ref is removed as well, a delete doesn't prune it 
automatically.
+   *
+   * @param trackingRefName the name of the remote tracking ref, e.g. 
refs/remotes/origin/feature
+   */
+  public boolean deleteRemoteBranch(String trackingRefName) throws 
HopException {
+    String[] remoteAndBranch = splitTrackingRefName(trackingRefName);
+    String remote = remoteAndBranch[0];
+    String branch = remoteAndBranch[1];
+
+    if (!push(remote, List.of(new RefSpec(":" + Constants.R_HEADS + branch)), 
true)) {
+      return false;
+    }
+    deleteTrackingRef(trackingRefName);
+    return true;
+  }
+
+  /**
+   * Rename a branch on the remote. Git has no rename over the wire: the 
branch is pushed under its
+   * new name first and only removed under the old name once that succeeded.
+   *
+   * @param trackingRefName the name of the remote tracking ref, e.g. 
refs/remotes/origin/feature
+   * @param newBranchName the new name of the branch on the remote, without 
the remote prefix
+   */
+  public boolean renameRemoteBranch(String trackingRefName, String 
newBranchName)
+      throws HopException {
+    String[] remoteAndBranch = splitTrackingRefName(trackingRefName);
+    String remote = remoteAndBranch[0];
+    String oldBranchName = remoteAndBranch[1];
+
+    try {
+      Ref ref = git.getRepository().findRef(trackingRefName);
+      if (ref == null) {
+        throw new HopException("Remote branch '" + trackingRefName + "' could 
not be found");
+      }
+
+      // Create the branch under its new name, at the commit the old one 
points at.
+      // Bail out if the remote rejects it: the old branch is all that is left 
otherwise.
+      //
+      String commitId = ref.getObjectId().name();
+      if (!push(
+          remote,
+          List.of(new RefSpec(commitId + ":" + Constants.R_HEADS + 
newBranchName)),
+          false)) {
+        return false;
+      }
+
+      if (!push(remote, List.of(new RefSpec(":" + Constants.R_HEADS + 
oldBranchName)), true)) {
+        return false;
+      }
+
+      // Keep the tracking refs in step, the rename is only picked up by the 
next fetch otherwise
+      //
+      updateTrackingRef(Constants.R_REMOTES + remote + "/" + newBranchName, 
ref.getObjectId());
+      deleteTrackingRef(trackingRefName);
+      return true;
+    } catch (HopException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new HopException(
+          "There was an error renaming remote branch '" + trackingRefName + 
"'", e);
+    }
+  }
+
+  /**
+   * Split a remote tracking ref name into the remote and the name of the 
branch on that remote:
+   * refs/remotes/origin/feature/hop becomes origin and feature/hop.
+   */
+  private String[] splitTrackingRefName(String trackingRefName) throws 
HopException {
+    if (!trackingRefName.startsWith(Constants.R_REMOTES)) {
+      throw new HopException("'" + trackingRefName + "' is not a remote 
branch");
+    }
+    // A branch name can contain slashes, so match on the configured remotes 
instead of splitting
+    // on the first slash.
+    //
+    String name = trackingRefName.substring(Constants.R_REMOTES.length());
+    for (String remote : git.getRepository().getRemoteNames()) {
+      if (name.startsWith(remote + "/")) {
+        return new String[] {remote, name.substring(remote.length() + 1)};
+      }
+    }
+    throw new HopException("No remote configured for remote branch '" + 
trackingRefName + "'");
+  }
+
+  /** Point a remote tracking ref at a commit locally, without touching the 
remote. */
+  private void updateTrackingRef(String trackingRefName, ObjectId objectId) 
throws HopException {
+    try {
+      RefUpdate update = git.getRepository().updateRef(trackingRefName);
+      update.setNewObjectId(objectId);
+      update.setForceUpdate(true);
+      update.update();
+    } catch (IOException e) {
+      throw new HopException("Unable to update remote tracking ref '" + 
trackingRefName + "'", e);
+    }
+  }
+
+  /** Remove a remote tracking ref locally, without touching the remote. */
+  private void deleteTrackingRef(String trackingRefName) throws HopException {
+    try {
+      RefUpdate update = git.getRepository().updateRef(trackingRefName);
+      update.setForceUpdate(true);
+      update.delete();
+    } catch (IOException e) {
+      throw new HopException("Unable to remove remote tracking ref '" + 
trackingRefName + "'", e);
+    }
+  }
+
+  /**
+   * Check whether a remote branch is the default branch (HEAD) of its remote. 
That is only known
+   * when the remote HEAD was fetched, so a false doesn't guarantee the branch 
is safe to remove:
+   * the remote has the final say.
+   *
+   * @param trackingRefName the name of the remote tracking ref, e.g. 
refs/remotes/origin/main
+   */
+  public boolean isRemoteHead(String trackingRefName) {
+    try {
+      String[] remoteAndBranch = splitTrackingRefName(trackingRefName);
+      Ref head = git.getRepository().exactRef(Constants.R_REMOTES + 
remoteAndBranch[0] + "/HEAD");
+      return head != null
+          && head.isSymbolic()
+          && head.getTarget().getName().equals(trackingRefName);
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
   private boolean push(RefSpec refSpec) throws HopException {
+    return push(Constants.DEFAULT_REMOTE_NAME, refSpec == null ? null : 
List.of(refSpec), true);
+  }
+
+  /**
+   * Push a set of refspecs to a remote.
+   *
+   * @param remote the name of the remote to push to
+   * @param refSpecs the refspecs to push, null to push the default refspec
+   * @param reportSuccess report a successful push to the user. Set this to 
false for a push that is
+   *     only a step in a larger operation, the caller reports on the end 
result then.
+   * @return true if every ref was updated on the remote
+   */
+  private boolean push(String remote, List<RefSpec> refSpecs, boolean 
reportSuccess)
+      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();
+      cmd.setRemote(remote);
 
-      String url = git.getRepository().getConfig().getString("remote", 
"origin", "url");
+      String url = git.getRepository().getConfig().getString("remote", remote, 
"url");
       if (!StringUtils.isEmpty(url) && (url.startsWith("https://";) || 
url.startsWith("http://";))) {
         cmd.setCredentialsProvider(credentialsProvider);
       }
 
-      if (refSpec != null) {
-        cmd.setRefSpecs(refSpec);
+      if (refSpecs != null) {
+        cmd.setRefSpecs(refSpecs);
       }
 
       Iterable<PushResult> resultIterable = cmd.call();
-      processPushResult(resultIterable);
-      return true;
+      return processPushResult(resultIterable, reportSuccess);
     } catch (TransportException e) {
       if (e.getMessage()
               .contains(
@@ -732,7 +875,7 @@ public class UIGit extends VCS {
           || e.getMessage()
               .contains(CONST_NOT_AUTHORIZED)) { // when the cached credential 
does not work
         if (promptUsernamePassword()) {
-          return push(refSpec);
+          return push(remote, refSpecs, reportSuccess);
         }
       } else {
         throw new HopException("There was an error doing a git push", e);
@@ -743,55 +886,64 @@ public class UIGit extends VCS {
     return false;
   }
 
-  private void processPushResult(Iterable<PushResult> resultIterable) {
-    resultIterable.forEach(
-        result -> { // for each (push)url
-          StringBuilder sb = new StringBuilder();
-          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(isTag ? "Tag name: " : "Branch name: ")
-                        // A delete has no source ref, report the ref on the 
remote instead
-                        
.append(Repository.shortenRefName(update.getRemoteName()))
+  /**
+   * Report on the outcome of a push.
+   *
+   * @return true if every ref was updated on the remote
+   */
+  private boolean processPushResult(Iterable<PushResult> resultIterable, 
boolean reportSuccess) {
+    boolean success = true;
+    for (PushResult result : resultIterable) { // for each (push)url
+      StringBuilder sb = new StringBuilder();
+      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(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(
+                            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");
-                    switch (update.getStatus()) {
-                      case REJECTED_NONFASTFORWARD:
-                        sb.append(" * ")
-                            .append(update.getStatus().toString())
-                            .append(
-                                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:
-                        sb.append(" * ")
-                            .append(update.getStatus().toString())
-                            .append(" - ")
-                            .append(update.getMessage() == null ? "" : "\n" + 
update.getMessage())
-                            .append("\n");
-                        break;
-                    }
-                  });
-          if (sb.isEmpty()) {
-            showMessageBox(
-                BaseMessages.getString(PKG, "Dialog.Success"),
-                BaseMessages.getString(PKG, "Dialog.Success"));
-          } else {
-            showMessageBox(BaseMessages.getString(PKG, CONST_DIALOG_ERROR), 
sb.toString());
-          }
-        });
+                    break;
+                  default:
+                    sb.append(" * ")
+                        .append(update.getStatus().toString())
+                        .append(" - ")
+                        .append(update.getMessage() == null ? "" : "\n" + 
update.getMessage())
+                        .append("\n");
+                    break;
+                }
+              });
+      if (sb.isEmpty()) {
+        if (reportSuccess) {
+          showMessageBox(
+              BaseMessages.getString(PKG, "Dialog.Success"),
+              BaseMessages.getString(PKG, "Dialog.Success"));
+        }
+      } else {
+        success = false;
+        showMessageBox(BaseMessages.getString(PKG, CONST_DIALOG_ERROR), 
sb.toString());
+      }
+    }
+    return success;
   }
 
   public String diff(String oldCommitId, String newCommitId) {
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 8e6aff175e..863dc8a946 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
@@ -31,12 +31,18 @@ 
GitGuiPlugin.Dialog.Branch.DeleteBranchConfirmation.Header=Delete a Branch
 GitGuiPlugin.Dialog.Branch.DeleteBranchConfirmation.Message=Are you sure you 
want to delete branch ''{0}'' ?
 GitGuiPlugin.Dialog.Branch.DeleteBranchSuccessFul.Header=Branch Deleted
 GitGuiPlugin.Dialog.Branch.DeleteBranchSuccessFul.Message=Branch deleted 
Successfully
+GitGuiPlugin.Dialog.Branch.DeleteRemoteBranchConfirmation.Header=Delete a 
Remote Branch
+GitGuiPlugin.Dialog.Branch.DeleteRemoteBranchConfirmation.Message=Are you sure 
you want to delete branch ''{0}'' on remote ''{1}'' ?\n\nThe branch is deleted 
for everyone using this repository.
+GitGuiPlugin.Dialog.Branch.InvalidName.Header=Invalid Branch Name
+GitGuiPlugin.Dialog.Branch.InvalidName.Message=''{0}'' is not a valid branch 
name.
 GitGuiPlugin.Dialog.Branch.MergeBranch.Header=Merge Branch
 GitGuiPlugin.Dialog.Branch.MergeBranch.Message=Select the branch you want to 
merge
 GitGuiPlugin.Dialog.Branch.MergeBranchSuccessFul.Header=Merge successful
 GitGuiPlugin.Dialog.Branch.MergeBranchSuccessFul.Message=Successful merge
 GitGuiPlugin.Dialog.Branch.RenameBranch.Header=Rename a Branch
 GitGuiPlugin.Dialog.Branch.RenameBranch.Message=New branch name
+GitGuiPlugin.Dialog.Branch.RenameRemoteBranchConfirmation.Header=Rename a 
Remote Branch
+GitGuiPlugin.Dialog.Branch.RenameRemoteBranchConfirmation.Message=Rename 
branch ''{0}'' to ''{1}'' on remote ''{2}'' ?\n\nGit has no rename on a remote: 
the branch is pushed under its new name and the old one is deleted. Anyone 
working with this branch has to update their local repository.
 GitGuiPlugin.Dialog.CherryPickCommit.Header=Cherry-pick commit
 GitGuiPlugin.Dialog.CherryPickCommitRepositoryIsNotClean.Message=Please clean 
your repository working tree before cherry-picking commit.
 GitGuiPlugin.Dialog.CherryPickCommitConflicts.Message=There were conflicts 
while cherry-picking commit ''{0}''\n\n{1}
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 eed022cf71..1e0b14e391 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
@@ -21,6 +21,8 @@ package org.apache.hop.git.model;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -41,6 +43,7 @@ import java.util.List;
 import java.util.Set;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.hop.core.exception.HopException;
 import org.apache.hop.git.model.revision.ObjectRevision;
 import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
 import org.eclipse.jgit.api.Git;
@@ -391,6 +394,84 @@ public class UIGitTest extends RepositoryTestCase {
     git2.close();
   }
 
+  @Test
+  public void testDeleteRemoteBranch() throws Exception {
+    Git git2 = new Git(db2);
+    UIGit uiGit2 = new UIGit();
+    uiGit2.setGit(git2);
+    setupRemote();
+
+    git.commit().setMessage("initial commit").call();
+    git.branchCreate().setName("feature/test").call();
+
+    // A branch name with a slash in it: the remote is origin, the branch is 
feature/test
+    assertTrue(uiGit.push(VCS.TYPE_BRANCH, "feature/test"));
+    git.fetch().call();
+    assertTrue(uiGit2.getLocalBranches().contains("feature/test"));
+    assertNotNull(db.findRef("refs/remotes/origin/feature/test"));
+
+    assertTrue(uiGit.deleteRemoteBranch("refs/remotes/origin/feature/test"));
+    assertFalse(uiGit2.getLocalBranches().contains("feature/test"));
+
+    // The tracking ref is removed as well, deleting on the remote doesn't 
prune it
+    assertNull(db.findRef("refs/remotes/origin/feature/test"));
+
+    git2.close();
+  }
+
+  @Test
+  public void testDeleteRemoteBranchWithoutRemote() throws Exception {
+    git.commit().setMessage("initial commit").call();
+
+    assertThrows(HopException.class, () -> 
uiGit.deleteRemoteBranch("refs/remotes/origin/feature"));
+  }
+
+  @Test
+  public void testRenameRemoteBranch() throws Exception {
+    Git git2 = new Git(db2);
+    UIGit uiGit2 = new UIGit();
+    uiGit2.setGit(git2);
+    setupRemote();
+
+    RevCommit commit = git.commit().setMessage("initial commit").call();
+    git.branchCreate().setName("old").call();
+    assertTrue(uiGit.push(VCS.TYPE_BRANCH, "old"));
+    git.fetch().call();
+
+    assertTrue(uiGit.renameRemoteBranch("refs/remotes/origin/old", "new"));
+
+    // The branch is created under its new name and removed under the old one, 
pointing at the
+    // same commit
+    assertTrue(uiGit2.getLocalBranches().contains("new"));
+    assertFalse(uiGit2.getLocalBranches().contains("old"));
+    assertEquals(commit.getId(), db2.resolve("refs/heads/new"));
+
+    // The tracking refs follow along, without needing a fetch
+    assertNull(db.findRef("refs/remotes/origin/old"));
+    assertNotNull(db.findRef("refs/remotes/origin/new"));
+
+    git2.close();
+  }
+
+  @Test
+  public void testIsRemoteHead() throws Exception {
+    setupRemote();
+
+    git.commit().setMessage("initial commit").call();
+    git.branchCreate().setName("feature").call();
+    assertTrue(uiGit.push(VCS.TYPE_BRANCH, "feature"));
+    git.fetch().call();
+
+    // Without a remote HEAD there is nothing to protect
+    assertFalse(uiGit.isRemoteHead("refs/remotes/origin/feature"));
+
+    
db.updateRef("refs/remotes/origin/HEAD").link("refs/remotes/origin/feature");
+    assertTrue(uiGit.isRemoteHead("refs/remotes/origin/feature"));
+
+    // Local branches and tags are never a remote HEAD
+    assertFalse(uiGit.isRemoteHead("refs/heads/master"));
+  }
+
   @Test
   public void testShouldPushOnlyToOrigin() throws Exception {
     // origin for db2

Reply via email to