aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDmitrii Naumenko <dmitrii.naumenko@jetbrains.com>2023-11-22 19:11:17 +0100
committerMatthias Sohn <matthias.sohn@sap.com>2024-01-28 16:13:32 +0100
commitc646649257070cd1707b7e86887e50c5acafa86c (patch)
treec7211b1548a6e02bf36bb39bac542b20e2597793
parent74471b8d755bdfe15a0585e49e1449788b4fc843 (diff)
downloadjgit-c646649257070cd1707b7e86887e50c5acafa86c.tar.gz
jgit-c646649257070cd1707b7e86887e50c5acafa86c.zip
CherryPick: add ability to customise cherry-picked commit message
Originally I wanted to support a feature similar to `-x` options from https://git-scm.com/docs/git-cherry-pick#_options. The idea was to append original commit hash in this format: ``` my original commit message (cherry picked from commit 75355897dc28e9975afed028c1a6d8c6b97b2a3c) ``` This can be useful information in some integrations. I decided to make it in a more generic way and pass custom `CherryPickCommitMessageProvider` implementation. One of the two default implementations can append original commit hash Change-Id: Id664e8438b0b76c5cb9b58113887eec04aa6f611
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java185
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java72
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java22
5 files changed, 309 insertions, 1 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
index 301d6be662..be3b33a9c5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
@@ -9,6 +9,8 @@
*/
package org.eclipse.jgit.api;
+import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL;
+import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -590,4 +592,187 @@ public class CherryPickCommandTest extends RepositoryTestCase {
checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n");
}
}
+
+ private void doCherryPickWithCustomProviderBaseTest(Git git,
+ CherryPickCommitMessageProvider commitMessageProvider)
+ throws Exception {
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitFirst = git.commit().setMessage("create fileA").call();
+
+ writeTrashFile("fileB", "content from file B\n");
+ git.add().addFilepattern("fileB").call();
+ RevCommit commitCreateFileB = git.commit()
+ .setMessage("create fileB\n\nsome commit details").call();
+
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitEditFileA1 = git.commit().setMessage("patch fileA 1")
+ .call();
+
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\nline 5\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitEditFileA2 = git.commit().setMessage("patch fileA 2")
+ .call();
+
+ git.branchCreate().setName("side").setStartPoint(commitFirst).call();
+ checkoutBranch("refs/heads/side");
+
+ CherryPickResult pickResult = git.cherryPick()
+ .setCherryPickCommitMessageProvider(commitMessageProvider)
+ .include(commitCreateFileB).include(commitEditFileA1)
+ .include(commitEditFileA2).call();
+
+ assertEquals(CherryPickStatus.OK, pickResult.getStatus());
+
+ assertTrue(new File(db.getWorkTree(), "fileA").exists());
+ assertTrue(new File(db.getWorkTree(), "fileB").exists());
+
+ checkFile(new File(db.getWorkTree(), "fileA"),
+ "line 1\nline 2\nline 3\nline 4\nline 5\n");
+ checkFile(new File(db.getWorkTree(), "fileB"), "content from file B\n");
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ @SuppressWarnings("boxing")
+ CherryPickCommitMessageProvider messageProvider = srcCommit -> {
+ String message = srcCommit.getFullMessage();
+ return String.format("%s (message length: %d)", message,
+ message.length());
+ };
+ doCherryPickWithCustomProviderBaseTest(git, messageProvider);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2 (message length: 13)",
+ history.next().getFullMessage());
+ assertEquals("patch fileA 1 (message length: 13)",
+ history.next().getFullMessage());
+ assertEquals(
+ "create fileB\n\nsome commit details (message length: 33)",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ doCherryPickWithCustomProviderBaseTest(git, ORIGINAL);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2", history.next().getFullMessage());
+ assertEquals("patch fileA 1", history.next().getFullMessage());
+ assertEquals("create fileB\n\nsome commit details",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ doCherryPickWithCustomProviderBaseTest(git,
+ ORIGINAL_WITH_REFERENCE);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2\n\n(cherry picked from commit 1ac121e90b0fb6fb18bbb4307e3e9731ceeba9e1)", history.next().getFullMessage());
+ assertEquals("patch fileA 1\n\n(cherry picked from commit 71475239df59076e18564fa360e3a74280926c2a)", history.next().getFullMessage());
+ assertEquals("create fileB\n\nsome commit details\n\n(cherry picked from commit 29b4501297ccf8de9de9f451e7beb384b51f5378)",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE_DonNotAddNewLineAfterFooter()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ CherryPickCommitMessageProvider commitMessageProvider = CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE;
+
+ RevCommit commit1 = addFileAndCommit(git, "file1", "content 1",
+ "commit1: no footer line");
+ RevCommit commit2 = addFileAndCommit(git, "file2", "content 2",
+ "commit2: simple single footer line"
+ + "\n\nSigned-off-by: Alice <alice@example.com>");
+ RevCommit commit3 = addFileAndCommit(git, "file3", "content 3",
+ "commit3: multiple footer lines\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>");
+ RevCommit commit4 = addFileAndCommit(git, "file4", "content 4",
+ "commit4: extra commit text before footer line\n\n"
+ + "Commit message details\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>");
+ RevCommit commit5 = addFileAndCommit(git, "file5", "content 5",
+ "commit5: extra commit text after footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n\n"
+ + "some extra description after footer");
+
+ git.branchCreate().setName("side").setStartPoint(commit1).call();
+ checkoutBranch("refs/heads/side");
+
+ CherryPickResult pickResult = git.cherryPick()
+ .setCherryPickCommitMessageProvider(commitMessageProvider)
+ .include(commit2).include(commit3).include(commit4)
+ .include(commit5).call();
+
+ assertEquals(CherryPickStatus.OK, pickResult.getStatus());
+
+ assertTrue(new File(db.getWorkTree(), "file1").exists());
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+ assertTrue(new File(db.getWorkTree(), "file3").exists());
+ assertTrue(new File(db.getWorkTree(), "file4").exists());
+ assertTrue(new File(db.getWorkTree(), "file5").exists());
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ RevCommit cpCommit1 = history.next();
+ RevCommit cpCommit2 = history.next();
+ RevCommit cpCommit3 = history.next();
+ RevCommit cpCommit4 = history.next();
+ RevCommit cpCommitInit = history.next();
+ assertFalse(history.hasNext());
+
+ assertEquals("commit5: extra commit text after footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n\n"
+ + "some extra description after footer\n\n"
+ + "(cherry picked from commit c3c9959207dc7ae7c83da5d36dc14ef2ca42d572)",
+ cpCommit1.getFullMessage());
+ assertEquals("commit4: extra commit text before footer line\n\n"
+ + "Commit message details\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n"
+ + "(cherry picked from commit af3e8106c12cb946a37b403ddb2dd6c11a883698)",
+ cpCommit2.getFullMessage());
+ assertEquals("commit3: multiple footer lines\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n"
+ + "(cherry picked from commit 6d60f1a70a11a32dff4402c157c4ac328c32ce6c)",
+ cpCommit3.getFullMessage());
+ assertEquals("commit2: simple single footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "(cherry picked from commit 92bf0ec458814ecc73da8e050e60547d2ea6cce5)",
+ cpCommit4.getFullMessage());
+
+ assertEquals("commit1: no footer line",
+ cpCommitInit.getFullMessage());
+ }
+ }
+
+ private RevCommit addFileAndCommit(Git git, String fileName,
+ String fileText, String commitMessage)
+ throws IOException, GitAPIException {
+ writeTrashFile(fileName, fileText);
+ git.add().addFilepattern(fileName).call();
+ return git.commit().setMessage(commitMessage).call();
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
index cac0743d68..657c3d242f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
@@ -291,18 +291,26 @@ public class FooterLineTest extends RepositoryTestCase {
f = footers.get(0);
assertEquals("Signed-off-by", f.getKey());
assertEquals("A. U. Thor <a@example.com>", f.getValue());
+ assertEquals(217, f.getStartOffset());
+ assertEquals(258, f.getEndOffset());
f = footers.get(1);
assertEquals("CC", f.getKey());
assertEquals("<some.mailing.list@example.com>", f.getValue());
+ assertEquals(259, f.getStartOffset());
+ assertEquals(305, f.getEndOffset());
f = footers.get(2);
assertEquals("Acked-by", f.getKey());
assertEquals("Some Reviewer <sr@example.com>", f.getValue());
+ assertEquals(356, f.getStartOffset());
+ assertEquals(396, f.getEndOffset());
f = footers.get(3);
assertEquals("Signed-off-by", f.getKey());
assertEquals("Main Tain Er <mte@example.com>", f.getValue());
+ assertEquals(397, f.getStartOffset());
+ assertEquals(442, f.getEndOffset());
}
@Test
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index 38e795b0eb..a1c64788bd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -9,6 +9,7 @@
*/
package org.eclipse.jgit.api;
+import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;
import java.io.IOException;
@@ -66,6 +67,8 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
private String ourCommitName = null;
+ private CherryPickCommitMessageProvider messageProvider = ORIGINAL;
+
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
private ContentMergeStrategy contentStrategy;
@@ -168,8 +171,10 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
dco.checkout();
if (!noCommit) {
try (Git git = new Git(getRepository())) {
+ String commitMessage = messageProvider
+ .getCherryPickedCommitMessage(srcCommit);
newHead = git.commit()
- .setMessage(srcCommit.getFullMessage())
+ .setMessage(commitMessage)
.setReflogComment(reflogPrefix + " " //$NON-NLS-1$
+ srcCommit.getShortMessage())
.setAuthor(srcCommit.getAuthorIdent())
@@ -297,6 +302,22 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
}
/**
+ * Set a message provider for a target cherry-picked commit<br>
+ * By default original commit message is used (see
+ * {@link CherryPickCommitMessageProvider#ORIGINAL})
+ *
+ * @param messageProvider
+ * the commit message provider
+ * @return {@code this}
+ * @since 6.9
+ */
+ public CherryPickCommand setCherryPickCommitMessageProvider(
+ CherryPickCommitMessageProvider messageProvider) {
+ this.messageProvider = messageProvider;
+ return this;
+ }
+
+ /**
* Set the prefix to use in the reflog.
* <p>
* This is primarily needed for implementing rebase in terms of
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java
new file mode 100644
index 0000000000..50d65b6fa8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 Dmitrii Naumenko <dmitrii.naumenko@jetbrains.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is aailable at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.api;
+
+import java.util.List;
+
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * The interface is used to construct a cherry-picked commit message based on
+ * the original commit
+ *
+ * @see #ORIGINAL
+ * @see #ORIGINAL_WITH_REFERENCE
+ * @since 6.9
+ */
+public interface CherryPickCommitMessageProvider {
+
+ /**
+ * This provider returns the original commit message
+ */
+ static final CherryPickCommitMessageProvider ORIGINAL = RevCommit::getFullMessage;
+
+ /**
+ * This provider returns the original commit message with original commit
+ * hash in SHA-1 form.<br>
+ * Example:
+ *
+ * <pre>
+ * <code>my original commit message
+ *
+ * (cherry picked from commit 75355897dc28e9975afed028c1a6d8c6b97b2a3c)</code>
+ * </pre>
+ *
+ * This is similar to <code>-x</code> flag in git-scm (see <a href=
+ * "https://git-scm.com/docs/git-cherry-pick#_options">https://git-scm.com/docs/git-cherry-pick#_options</a>)
+ */
+ static final CherryPickCommitMessageProvider ORIGINAL_WITH_REFERENCE = srcCommit -> {
+ String fullMessage = srcCommit.getFullMessage();
+
+ // Don't add extra new line after footer (aka trailer)
+ // https://stackoverflow.com/questions/70007405/git-log-exclude-cherry-pick-messages-for-trailers
+ // https://lore.kernel.org/git/7vmx136cdc.fsf@alter.siamese.dyndns.org
+ String separator = messageEndsWithFooter(srcCommit) ? "\n" : "\n\n"; //$NON-NLS-1$//$NON-NLS-2$
+ String revisionString = srcCommit.getName();
+ return String.format("%s%s(cherry picked from commit %s)", //$NON-NLS-1$
+ fullMessage, separator, revisionString);
+ };
+
+ /**
+ * @param srcCommit
+ * original cherry-picked commit
+ * @return target cherry-picked commit message
+ */
+ String getCherryPickedCommitMessage(RevCommit srcCommit);
+
+ private static boolean messageEndsWithFooter(RevCommit srcCommit) {
+ byte[] rawBuffer = srcCommit.getRawBuffer();
+ List<FooterLine> footers = srcCommit.getFooterLines();
+ int maxFooterEnd = footers.stream().mapToInt(FooterLine::getEndOffset)
+ .max().orElse(-1);
+ return rawBuffer.length == maxFooterEnd;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
index 227ea0fd65..fa7fb316c9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java
@@ -251,6 +251,28 @@ public final class FooterLine {
return RawParseUtils.decode(enc, buffer, lt, gt - 1);
}
+ /**
+ * @return start offset of the footer relative to the original raw message
+ * byte buffer
+ *
+ * @see #fromMessage(byte[])
+ * @since 6.9
+ */
+ public int getStartOffset() {
+ return keyStart;
+ }
+
+ /**
+ * @return end offset of the footer relative to the original raw message
+ * byte buffer
+ *
+ * @see #fromMessage(byte[])
+ * @since 6.9
+ */
+ public int getEndOffset() {
+ return valEnd;
+ }
+
@Override
public String toString() {
return getKey() + ": " + getValue(); //$NON-NLS-1$