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