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 87ac0ff46c improved messages/dialogs when merging branches. fixes 
#7691 (#7697)
87ac0ff46c is described below

commit 87ac0ff46c21e06b06ab868e5dc6e625261b62b3
Author: Bart Maertens <[email protected]>
AuthorDate: Fri Jul 31 09:03:42 2026 +0200

    improved messages/dialogs when merging branches. fixes #7691 (#7697)
---
 .../org/apache/hop/git/GitCommitPerspective.java   | 111 ++++++++++++----
 .../main/java/org/apache/hop/git/GitGuiPlugin.java |  10 +-
 .../main/java/org/apache/hop/git/model/UIGit.java  | 147 +++++++++++++++++++--
 .../hop/git/messages/messages_en_US.properties     |   4 +
 .../git/model/messages/messages_en_US.properties   |   9 ++
 .../java/org/apache/hop/git/model/UIGitTest.java   |  97 ++++++++++++++
 6 files changed, 339 insertions(+), 39 deletions(-)

diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
index b919e537cf..82c1f2ff2b 100644
--- 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
+++ 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
@@ -67,6 +67,7 @@ import org.eclipse.jgit.lib.Constants;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CLabel;
 import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
@@ -117,7 +118,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
   private Control wToolBar;
   private Tree wTree;
   private Text wMessage;
-  private CLabel wError;
+  private CLabel wStatus;
   private Button wAmend;
   private Button wCommit;
   private Button wCommitAndPush;
@@ -297,16 +298,15 @@ public class GitCommitPerspective implements 
IHopPerspective {
     wCommitAndPush.addListener(SWT.Selection, event -> commitFiles(true));
     PropsUi.setLook(wCommitAndPush);
 
-    wError = new CLabel(composite, SWT.NONE);
-    wError.setImage(GuiResource.getInstance().getImageError());
-    wError.setLayoutData(
+    wStatus = new CLabel(composite, SWT.NONE);
+    wStatus.setLayoutData(
         FormDataBuilder.builder()
             .bottom()
             .left(wCommitAndPush, ConstUi.MEDIUM_MARGIN)
             .right()
             .result());
-    wError.setVisible(false);
-    PropsUi.setLook(wError);
+    wStatus.setVisible(false);
+    PropsUi.setLook(wStatus);
 
     wMessage = new Text(composite, SWT.BORDER | SWT.MULTI);
     wMessage.setLayoutData(
@@ -329,24 +329,32 @@ public class GitCommitPerspective implements 
IHopPerspective {
   @GuiKeyboardShortcut(control = true, key = 'k', global = true)
   @GuiOsxKeyboardShortcut(command = true, key = 'k', global = true)
   public void selectAllStaged() {
+    checkStagedFiles();
+
+    wMessage.selectAll();
+    wMessage.setFocus();
+  }
+
+  /**
+   * Check the staged files and uncheck the unstaged ones. Staged files are 
the ones that go into
+   * the next commit, so they are what the commit buttons act on by default.
+   */
+  private void checkStagedFiles() {
+    if (wTree == null || wTree.isDisposed() || wTree.getItemCount() < 2) {
+      return;
+    }
+
     TreeItem stagedRootItem = this.getStagedRootItem();
-    if (stagedRootItem != null) {
-      stagedRootItem.setChecked(true);
-      for (TreeItem item : stagedRootItem.getItems()) {
-        item.setChecked(true);
-      }
+    stagedRootItem.setChecked(stagedRootItem.getItemCount() > 0);
+    for (TreeItem item : stagedRootItem.getItems()) {
+      item.setChecked(true);
     }
 
     TreeItem unstagedRootItem = this.getUnstagedRootItem();
-    if (unstagedRootItem != null) {
-      unstagedRootItem.setChecked(false);
-      for (TreeItem item : unstagedRootItem.getItems()) {
-        item.setChecked(false);
-      }
+    unstagedRootItem.setChecked(false);
+    for (TreeItem item : unstagedRootItem.getItems()) {
+      item.setChecked(false);
     }
-
-    wMessage.selectAll();
-    wMessage.setFocus();
   }
 
   protected void retrieveState() {
@@ -710,10 +718,18 @@ public class GitCommitPerspective implements 
IHopPerspective {
       List<UIFile> filesToIgnore =
           this.getSelectedFiles(getStagedRootItem().getItems(), new 
ArrayList<>(), false);
 
-      // No files to commit
+      // No files to commit. Selecting a row in the tree only highlights it, 
so tell the user to
+      // check the box when there are files listed but none of them are 
checked.
+      //
       if (filesToCommit.isEmpty()) {
-        wError.setText(BaseMessages.getString(PKG, 
"GitGuiPlugin.Dialog.NoFilesToCommit.Message"));
-        wError.setVisible(true);
+        boolean treeHasFiles = !filesToIgnore.isEmpty() || 
!getUncheckedFiles().isEmpty();
+        showStatus(
+            GuiResource.getInstance().getImageError(),
+            BaseMessages.getString(
+                PKG,
+                treeHasFiles
+                    ? "GitCommitPerspective.Error.NoFilesChecked.Message"
+                    : "GitGuiPlugin.Dialog.NoFilesToCommit.Message"));
         return;
       }
 
@@ -737,6 +753,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
       // Commit selected files
       uiGit.commit(authorName, message, amend);
+      String commitId = uiGit.getCommitId(Constants.HEAD);
 
       // Restore unselected staged files
       if (!filesToIgnore.isEmpty()) {
@@ -755,9 +772,10 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
       wAmend.setSelection(false);
 
+      boolean pushed = false;
       if (push) {
         try {
-          uiGit.push();
+          pushed = uiGit.push();
         } catch (Exception e) {
           new ErrorDialog(
               HopGui.getInstance().getShell(),
@@ -766,6 +784,19 @@ public class GitCommitPerspective implements 
IHopPerspective {
               e);
         }
       }
+
+      // Confirm what happened. The status stays until the next thing the user 
does, no dialog to
+      // click away. It goes last: refreshing the tree clears the status line.
+      //
+      showStatus(
+          GuiResource.getInstance().getImageSuccess(),
+          BaseMessages.getString(
+              PKG,
+              pushed
+                  ? "GitCommitPerspective.Status.CommittedAndPushed.Message"
+                  : "GitCommitPerspective.Status.Committed.Message",
+              Integer.toString(filesToCommit.size()),
+              getCommitIdLabel(commitId)));
     } catch (Exception e) {
       new ErrorDialog(
           HopGui.getInstance().getShell(),
@@ -775,6 +806,27 @@ public class GitCommitPerspective implements 
IHopPerspective {
     }
   }
 
+  /**
+   * Show a line of feedback next to the commit buttons. It is wiped by the 
next {@link
+   * #updateGui()} so it lives exactly as long as the user's next action.
+   */
+  private void showStatus(Image image, String message) {
+    wStatus.setImage(image);
+    wStatus.setText(message);
+    wStatus.setVisible(true);
+  }
+
+  /**
+   * The short commit id to append to the status, or nothing at all if git did 
not hand one back.
+   */
+  private String getCommitIdLabel(String commitId) {
+    if (commitId == null || commitId.length() < 7) {
+      return "";
+    }
+    return BaseMessages.getString(
+        PKG, "GitCommitPerspective.Status.CommitId.Label", 
commitId.substring(0, 7));
+  }
+
   private TreeItem getStagedRootItem() {
     return wTree.getItems()[0];
   }
@@ -810,6 +862,13 @@ public class GitCommitPerspective implements 
IHopPerspective {
     return getSelectedFiles(getUnstagedRootItem().getItems(), new 
ArrayList<>(), true);
   }
 
+  protected List<UIFile> getUncheckedFiles() {
+    if (wTree == null || wTree.isDisposed()) {
+      return List.of();
+    }
+    return getSelectedFiles(wTree.getItems(), new ArrayList<>(), false);
+  }
+
   protected List<UIFile> getSelectedFiles(TreeItem[] items, List<UIFile> 
files, boolean selected) {
     for (TreeItem item : items) {
       if ((item.getData() instanceof UIFile file) && item.getChecked() == 
selected) {
@@ -833,7 +892,7 @@ public class GitCommitPerspective implements 
IHopPerspective {
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_RESTORE, 
!getSelectedStagedFiles().isEmpty());
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_DELETE, 
!getSelectedFiles().isEmpty());
 
-    wError.setVisible(false);
+    wStatus.setVisible(false);
     wCommit.setEnabled(commitEnabled);
     wCommitAndPush.setEnabled(commitEnabled);
   }
@@ -904,6 +963,10 @@ public class GitCommitPerspective implements 
IHopPerspective {
 
         stagedItem.setExpanded(true);
         untrackedItem.setExpanded(true);
+
+        // Start from the staged files, the way a "git commit" would
+        //
+        checkStagedFiles();
       }
       wTree.setRedraw(true);
 
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
index f89aa9099e..bf47c3a671 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
@@ -292,10 +292,14 @@ public class GitGuiPlugin
       image = "pull.svg")
   public void gitPull() {
     try {
-      if (git.pull()) {
-        // Refresh the explorer file, refs and commit history
-        GitPerspective.getInstance().refresh(true);
+      boolean merged = git.pull();
 
+      // Refresh the explorer file, refs and commit history. A pull fetches 
the remote refs even
+      // when there was nothing to merge into the current branch.
+      //
+      GitPerspective.getInstance().refresh(true);
+
+      if (merged) {
         MessageBox pullSuccessful =
             new MessageBox(HopGui.getInstance().getShell(), 
SWT.ICON_INFORMATION);
         pullSuccessful.setText(
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 eb79851d5c..8944fcb759 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
@@ -29,12 +29,14 @@ import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import lombok.Getter;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -64,6 +66,7 @@ import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.api.RevertCommand;
 import org.eclipse.jgit.api.Status;
 import org.eclipse.jgit.api.StatusCommand;
+import org.eclipse.jgit.api.errors.CheckoutConflictException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.diff.DiffEntry;
@@ -123,6 +126,9 @@ public class UIGit extends VCS {
           "Authentication is required but no CredentialsProvider has been 
registered";
   public static final String CONST_NOT_AUTHORIZED = "not authorized";
   public static final String CONST_DIALOG_ERROR = "Dialog.Error";
+  public static final String CONST_MERGE_FAILED_HEADER = 
"UIGit.Dialog.MergeFailed.Header";
+  public static final String CONST_MERGE_FAILED_LOCAL_CHANGES =
+      "UIGit.Dialog.MergeFailed.LocalChanges.Message";
 
   static {
     /**
@@ -1077,22 +1083,52 @@ public class UIGit extends VCS {
     try {
       ObjectId obj = git.getRepository().resolve(value);
       MergeResult result = 
git.merge().include(obj).setStrategy(mergeStrategy).call();
-      if (result.getMergeStatus().isSuccessful()) {
-        return true;
-      } else {
-        // TODO: get rid of message box
+      if (result.getMergeStatus() == MergeStatus.ALREADY_UP_TO_DATE) {
+        // The merge succeeded but did nothing. That is surprising to anyone 
with work in progress,
+        // so point out that staged or modified files are not part of a branch 
yet.
         //
         showMessageBox(
-            BaseMessages.getString(PKG, CONST_DIALOG_ERROR), 
result.getMergeStatus().toString());
-        if (result.getMergeStatus() == MergeStatus.CONFLICTING) {
-          Map<String, int[][]> conflicts = result.getConflicts();
-          for (String path : conflicts.keySet()) {
-            checkout(path, Constants.HEAD, CONST_OURS);
-            checkout(path, getExpandedName(value, VCS.TYPE_BRANCH), 
CONST_THEIRS);
-          }
-          return true;
+            BaseMessages.getString(PKG, "UIGit.Dialog.NothingToMerge.Header"),
+            BaseMessages.getString(
+                PKG,
+                "UIGit.Dialog.NothingToMerge.Message",
+                value,
+                getBranch(),
+                getUncommittedChangesBlock()));
+        return false;
+      }
+      if (result.getMergeStatus().isSuccessful()) {
+        return true;
+      }
+
+      boolean conflicting = result.getMergeStatus() == MergeStatus.CONFLICTING;
+      if (conflicting) {
+        Map<String, int[][]> conflicts = result.getConflicts();
+        for (String path : conflicts.keySet()) {
+          checkout(path, Constants.HEAD, CONST_OURS);
+          checkout(path, getExpandedName(value, VCS.TYPE_BRANCH), 
CONST_THEIRS);
         }
       }
+
+      // TODO: get rid of message box
+      //
+      showMessageBox(
+          BaseMessages.getString(PKG, CONST_MERGE_FAILED_HEADER),
+          getMergeFailureMessage(value, result));
+
+      return conflicting;
+    } catch (CheckoutConflictException e) {
+      // A fast-forward merge never starts when local changes are in the way. 
Explain that instead
+      // of showing a stack trace.
+      //
+      showMessageBox(
+          BaseMessages.getString(PKG, CONST_MERGE_FAILED_HEADER),
+          BaseMessages.getString(
+              PKG,
+              CONST_MERGE_FAILED_LOCAL_CHANGES,
+              value,
+              getBranch(),
+              getPathList(e.getConflictingPaths())));
       return false;
     } catch (Exception e) {
       throw new HopException(
@@ -1100,6 +1136,93 @@ public class UIGit extends VCS {
     }
   }
 
+  /**
+   * JGit only reports a terse status like "Failed" or "Conflicting". Turn 
that into an explanation
+   * the user can act on: which files are in the way and what to do about them.
+   *
+   * @param value the branch, tag or commit that was merged in
+   * @param result the unsuccessful merge result
+   * @return a message describing the failure and how to recover from it
+   */
+  private String getMergeFailureMessage(String value, MergeResult result) {
+    String currentBranch = getBranch();
+    switch (result.getMergeStatus()) {
+      case FAILED:
+        return getLocalChangesMessage(
+            value,
+            currentBranch,
+            result.getFailingPaths() == null ? null : 
result.getFailingPaths().keySet(),
+            result);
+      case CHECKOUT_CONFLICT:
+        return getLocalChangesMessage(value, currentBranch, 
result.getCheckoutConflicts(), result);
+      case CONFLICTING:
+        return BaseMessages.getString(
+            PKG,
+            "UIGit.Dialog.MergeFailed.Conflicting.Message",
+            value,
+            currentBranch,
+            getPathList(result.getConflicts() == null ? Set.of() : 
result.getConflicts().keySet()));
+      case ABORTED:
+        return BaseMessages.getString(
+            PKG, "UIGit.Dialog.MergeFailed.Aborted.Message", value, 
currentBranch);
+      default:
+        return getGenericMessage(value, currentBranch, result);
+    }
+  }
+
+  /**
+   * Local changes are in the way. Naming them is the whole point of the 
message, so fall back on
+   * the plain status when JGit did not tell us which files those are.
+   */
+  private String getLocalChangesMessage(
+      String value, String currentBranch, Collection<String> paths, 
MergeResult result) {
+    if (paths == null || paths.isEmpty()) {
+      return getGenericMessage(value, currentBranch, result);
+    }
+    return BaseMessages.getString(
+        PKG, CONST_MERGE_FAILED_LOCAL_CHANGES, value, currentBranch, 
getPathList(paths));
+  }
+
+  private String getGenericMessage(String value, String currentBranch, 
MergeResult result) {
+    return BaseMessages.getString(
+        PKG,
+        "UIGit.Dialog.MergeFailed.Generic.Message",
+        value,
+        currentBranch,
+        result.getMergeStatus().toString());
+  }
+
+  /**
+   * Nothing to merge usually means the work is still sitting in the index or 
the working tree
+   * instead of on the branch. List those files so the user can see where 
their changes are.
+   *
+   * @return the uncommitted changes to mention, or an empty string when the 
working tree is clean
+   */
+  private String getUncommittedChangesBlock() {
+    try {
+      Status status = git.status().call();
+      if (!status.hasUncommittedChanges()) {
+        return "";
+      }
+      Set<String> paths = new TreeSet<>();
+      paths.addAll(status.getAdded());
+      paths.addAll(status.getChanged());
+      paths.addAll(status.getRemoved());
+      paths.addAll(status.getModified());
+      paths.addAll(status.getMissing());
+      return BaseMessages.getString(
+          PKG, "UIGit.Dialog.NothingToMerge.UncommittedChanges.Label", 
getPathList(paths));
+    } catch (NoWorkTreeException | GitAPIException e) {
+      return "";
+    }
+  }
+
+  private String getPathList(Collection<String> paths) {
+    StringBuilder list = new StringBuilder();
+    paths.forEach(path -> list.append(" - ").append(path).append(Const.CR));
+    return list.toString();
+  }
+
   private boolean hasUncommittedChanges() {
     try {
       return git.status().call().hasUncommittedChanges();
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 83b36c1b19..aa456a7190 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
@@ -121,6 +121,10 @@ GitCommitPerspective.Menu.Restore.Text=Restore...
 GitCommitPerspective.Menu.ShowTextDiff.Text=Show text diff
 GitCommitPerspective.Menu.ShowGraphDiff.Text=Show visual diff
 GitCommitPerspective.Menu.Delete.Text=Delete...
+GitCommitPerspective.Error.NoFilesChecked.Message=Please check the box in 
front of the files you want to commit
+GitCommitPerspective.Status.Committed.Message=Committed {0} file(s) {1}
+GitCommitPerspective.Status.CommittedAndPushed.Message=Committed and pushed 
{0} file(s) {1}
+GitCommitPerspective.Status.CommitId.Label=as {0}
 GitCommitPerspective.Status.Staged.Label=Staged
 GitCommitPerspective.Status.Unstaged.Label=Unstaged
 GitCommitPerspective.Toolbar.AddToGit.Tooltip=Add
diff --git 
a/plugins/misc/git/src/main/resources/org/apache/hop/git/model/messages/messages_en_US.properties
 
b/plugins/misc/git/src/main/resources/org/apache/hop/git/model/messages/messages_en_US.properties
index 93ca93b4d5..2fe94624ea 100644
--- 
a/plugins/misc/git/src/main/resources/org/apache/hop/git/model/messages/messages_en_US.properties
+++ 
b/plugins/misc/git/src/main/resources/org/apache/hop/git/model/messages/messages_en_US.properties
@@ -19,3 +19,12 @@
 Dialog.Error=Error
 Dialog.Success=Success
 Git.Dialog.UncommittedChanges.Message=There are uncommitted changes
+
+UIGit.Dialog.MergeFailed.Header=Merge failed
+UIGit.Dialog.MergeFailed.LocalChanges.Message=Your local changes to the 
following files would be overwritten by merging ''{0}'' into 
''{1}'':\n{2}\nPlease commit or revert your changes before you merge.
+UIGit.Dialog.MergeFailed.Conflicting.Message=Merging ''{0}'' into ''{1}'' ran 
into conflicts:\n{2}\nA ''.ours'' and a ''.theirs'' copy was written next to 
every conflicting file. Resolve the conflicts and commit the result.
+UIGit.Dialog.MergeFailed.Aborted.Message=The merge of ''{0}'' into ''{1}'' was 
aborted. Your files were left untouched.
+UIGit.Dialog.MergeFailed.Generic.Message=''{0}'' was not merged into ''{1}''. 
Git reported: {2}
+UIGit.Dialog.NothingToMerge.Header=Nothing to merge
+UIGit.Dialog.NothingToMerge.Message=''{1}'' is already up to date with 
''{0}''.{2}
+UIGit.Dialog.NothingToMerge.UncommittedChanges.Label=\n\nYou have uncommitted 
changes:\n{0}
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 89a1486176..dcbe60aab5 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
@@ -29,6 +29,7 @@ import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import java.io.File;
 import java.io.InputStream;
@@ -48,12 +49,14 @@ import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.URIish;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
 public class UIGitTest extends RepositoryTestCase {
   private Git git;
@@ -511,6 +514,100 @@ public class UIGitTest extends RepositoryTestCase {
     assertFalse(file.exists());
   }
 
+  @Test
+  public void testMergeBranchWithUncommittedChangesExplainsWhatToDo() throws 
Exception {
+    initialCommit();
+    commitOnBranch("develop", "Test.txt", "Hello from develop");
+
+    // Let master move on as well, so merging develop is a real merge and not 
a fast-forward
+    //
+    git.checkout().setName(Constants.MASTER).call();
+    writeTrashFile("Other.txt", "Hello master");
+    git.add().addFilepattern("Other.txt").call();
+    git.commit().setMessage("master commit").call();
+
+    // Leave an uncommitted change in the file the merge needs to touch
+    //
+    writeTrashFile("Test.txt", "Uncommitted work");
+
+    assertFalse(uiGit.mergeBranch("develop", MergeStrategy.RECURSIVE));
+    assertMergeFailureExplained();
+  }
+
+  @Test
+  public void testFastForwardMergeWithUncommittedChangesExplainsWhatToDo() 
throws Exception {
+    initialCommit();
+    commitOnBranch("develop", "Test.txt", "Hello from develop");
+
+    // Master is untouched, so merging develop fast-forwards. The checkout of 
Test.txt can't happen
+    // while it holds uncommitted changes.
+    //
+    git.checkout().setName(Constants.MASTER).call();
+    writeTrashFile("Test.txt", "Uncommitted work");
+
+    assertFalse(uiGit.mergeBranch("develop", MergeStrategy.RECURSIVE));
+    assertMergeFailureExplained();
+  }
+
+  @Test
+  public void 
testMergeBranchWithStagedButUncommittedWorkIsNotReportedAsSuccess() throws 
Exception {
+    initialCommit();
+
+    // Stage a file on the branch but never commit it, so the branch holds no 
commits of its own
+    //
+    git.branchCreate().setName("test-branch").call();
+    git.checkout().setName("test-branch").call();
+    writeTrashFile("bogus-pipeline.hpl", "not committed");
+    git.add().addFilepattern("bogus-pipeline.hpl").call();
+
+    git.checkout().setName(Constants.MASTER).call();
+
+    // There is nothing to merge, so this is not a successful merge
+    //
+    assertFalse(uiGit.mergeBranch("test-branch", MergeStrategy.RECURSIVE));
+
+    ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
+    verify(uiGit).showMessageBox(anyString(), message.capture());
+
+    assertTrue(message.getValue(), message.getValue().contains("is already up 
to date with"));
+    assertTrue(message.getValue(), message.getValue().contains("You have 
uncommitted changes"));
+    assertTrue(message.getValue(), 
message.getValue().contains("bogus-pipeline.hpl"));
+  }
+
+  @Test
+  public void 
testMergeBranchUpToDateWithCleanWorkingTreeDoesNotMentionUncommittedChanges()
+      throws Exception {
+    initialCommit();
+    git.branchCreate().setName("test-branch").call();
+
+    assertFalse(uiGit.mergeBranch("test-branch", MergeStrategy.RECURSIVE));
+
+    ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
+    verify(uiGit).showMessageBox(anyString(), message.capture());
+
+    assertTrue(message.getValue(), message.getValue().contains("is already up 
to date with"));
+    assertFalse(message.getValue().contains("You have uncommitted changes"));
+  }
+
+  /** The user needs to know which file is in the way and how to get past it. 
*/
+  private void assertMergeFailureExplained() {
+    ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
+    verify(uiGit).showMessageBox(anyString(), message.capture());
+
+    assertTrue(message.getValue(), message.getValue().contains("Test.txt"));
+    assertTrue(
+        message.getValue(),
+        message.getValue().contains("Please commit or revert your changes 
before you merge"));
+  }
+
+  private void commitOnBranch(String branch, String file, String content) 
throws Exception {
+    git.branchCreate().setName(branch).call();
+    git.checkout().setName(branch).call();
+    writeTrashFile(file, content);
+    git.add().addFilepattern(file).call();
+    git.commit().setMessage(branch + " commit").call();
+  }
+
   private RevCommit initialCommit() throws Exception {
     writeTrashFile("Test.txt", "Hello world");
     git.add().addFilepattern("Test.txt").call();

Reply via email to