diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java new file mode 100644 index 000000000..d95d7814e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022, Thomas Wolf and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; +import org.junit.Test; + +public class CommitConfigTest { + + @Test + public void testDefaults() throws Exception { + CommitConfig cfg = parse(""); + assertEquals("Unexpected clean-up mode", CleanupMode.DEFAULT, + cfg.getCleanupMode()); + } + + @Test + public void testCommitCleanup() throws Exception { + String[] values = { "strip", "whitespace", "verbatim", "scissors", + "default" }; + CleanupMode[] expected = { CleanupMode.STRIP, CleanupMode.WHITESPACE, + CleanupMode.VERBATIM, CleanupMode.SCISSORS, + CleanupMode.DEFAULT }; + for (int i = 0; i < values.length; i++) { + CommitConfig cfg = parse("[commit]\n\tcleanup = " + values[i]); + assertEquals("Unexpected clean-up mode", expected[i], + cfg.getCleanupMode()); + } + } + + @Test + public void testResolve() throws Exception { + String[] values = { "strip", "whitespace", "verbatim", "scissors", + "default" }; + CleanupMode[] expected = { CleanupMode.STRIP, CleanupMode.WHITESPACE, + CleanupMode.VERBATIM, CleanupMode.SCISSORS, + CleanupMode.DEFAULT }; + for (int i = 0; i < values.length; i++) { + CommitConfig cfg = parse("[commit]\n\tcleanup = " + values[i]); + for (CleanupMode mode : CleanupMode.values()) { + for (int j = 0; j < 2; j++) { + CleanupMode resolved = cfg.resolve(mode, j == 0); + if (mode != CleanupMode.DEFAULT) { + assertEquals("Clean-up mode should be unchanged", mode, + resolved); + } else if (i + 1 < values.length) { + assertEquals("Unexpected clean-up mode", expected[i], + resolved); + } else { + assertEquals("Unexpected clean-up mode", + j == 0 ? CleanupMode.STRIP + : CleanupMode.WHITESPACE, + resolved); + } + } + } + } + } + + @Test + public void testCleanDefaultThrows() throws Exception { + assertThrows(IllegalArgumentException.class, () -> CommitConfig + .cleanText("Whatever", CleanupMode.DEFAULT, '#')); + } + + @Test + public void testCleanVerbatim() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.VERBATIM, '#')); + } + + @Test + public void testCleanWhitespace() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# A comment\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.WHITESPACE, '#')); + } + + @Test + public void testCleanStrip() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", "Whatever\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.STRIP, '#')); + } + + @Test + public void testCleanStripCustomChar() throws Exception { + String message = "\n \nWhatever \n\n\n# Not a comment\n\n 8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# Not a comment\n\n 8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# Not a comment\n\n 8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsNoScissor3() throws Exception { + String message = "Text\n" + // Wrong number of dashes + + "# ----------------------- >8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsAtEnd() throws Exception { + String message = "Text\n" + + "# ------------------------ >8 ------------------------\n"; + assertEquals("Unexpected message change", "Text\n", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + private static CommitConfig parse(String content) + throws ConfigInvalidException { + Config c = new Config(); + c.fromText(content); + return c.get(CommitConfig.KEY); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java index 22e1f9818..55cc02683 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java @@ -18,11 +18,13 @@ import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.text.MessageFormat; +import java.util.Locale; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; @@ -34,22 +36,76 @@ * @since 5.13 */ public class CommitConfig { + /** * Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser KEY = CommitConfig::new; + private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$ + + /** + * How to clean up commit messages when committing. + * + * @since 6.1 + */ + public enum CleanupMode implements ConfigEnum { + + /** + * {@link #WHITESPACE}, additionally remove comment lines. + */ + STRIP, + + /** + * Remove trailing whitespace and leading and trailing empty lines; + * collapse multiple empty lines to a single one. + */ + WHITESPACE, + + /** + * Make no changes. + */ + VERBATIM, + + /** + * Omit everything from the first "scissor" line on, then apply + * {@link #WHITESPACE}. + */ + SCISSORS, + + /** + * Use {@link #STRIP} for user-edited messages, otherwise + * {@link #WHITESPACE}, unless overridden by a git config setting other + * than DEFAULT. + */ + DEFAULT; + + @Override + public String toConfigValue() { + return name().toLowerCase(Locale.ROOT); + } + + @Override + public boolean matchConfigValue(String in) { + return toConfigValue().equals(in); + } + } + private final static Charset DEFAULT_COMMIT_MESSAGE_ENCODING = StandardCharsets.UTF_8; private String i18nCommitEncoding; private String commitTemplatePath; + private CleanupMode cleanupMode; + private CommitConfig(Config rc) { commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION, null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE); i18nCommitEncoding = rc.getString(ConfigConstants.CONFIG_SECTION_I18N, null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING); + cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null, + ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT); } /** @@ -74,6 +130,48 @@ public String getCommitEncoding() { return i18nCommitEncoding; } + /** + * Retrieves the {@link CleanupMode} as given by git config + * {@code commit.cleanup}. + * + * @return the {@link CleanupMode}; {@link CleanupMode#DEFAULT} if the git + * config is not set + * @since 6.1 + */ + @NonNull + public CleanupMode getCleanupMode() { + return cleanupMode; + } + + /** + * Computes a non-default {@link CleanupMode} from the given mode and the + * git config. + * + * @param mode + * {@link CleanupMode} to resolve + * @param defaultStrip + * if {@code true} return {@link CleanupMode#STRIP} if the git + * config is also "default", otherwise return + * {@link CleanupMode#WHITESPACE} + * @return the {@code mode}, if it is not {@link CleanupMode#DEFAULT}, + * otherwise the resolved mode, which is never + * {@link CleanupMode#DEFAULT} + * @since 6.1 + */ + @NonNull + public CleanupMode resolve(@NonNull CleanupMode mode, + boolean defaultStrip) { + if (CleanupMode.DEFAULT == mode) { + CleanupMode defaultMode = getCleanupMode(); + if (CleanupMode.DEFAULT == defaultMode) { + return defaultStrip ? CleanupMode.STRIP + : CleanupMode.WHITESPACE; + } + return defaultMode; + } + return mode; + } + /** * Get the content to the commit template as defined in * {@code commit.template}. If no {@code i18n.commitEncoding} is specified, @@ -135,4 +233,86 @@ private Charset getEncoding() throws ConfigInvalidException { return commitMessageEncoding; } + + /** + * Processes a text according to the given {@link CleanupMode}. + * + * @param text + * text to process + * @param mode + * {@link CleanupMode} to use + * @param commentChar + * comment character (normally {@code #}) to use if {@code mode} + * is {@link CleanupMode#STRIP} or {@link CleanupMode#SCISSORS} + * @return the processed text + * @throws IllegalArgumentException + * if {@code mode} is {@link CleanupMode#DEFAULT} (use + * {@link #resolve(CleanupMode, boolean)} first) + * @since 6.1 + */ + public static String cleanText(@NonNull String text, + @NonNull CleanupMode mode, char commentChar) { + String toProcess = text; + boolean strip = false; + switch (mode) { + case VERBATIM: + return text; + case SCISSORS: + String cut = commentChar + CUT; + if (text.startsWith(cut)) { + return ""; //$NON-NLS-1$ + } + int cutPos = text.indexOf('\n' + cut); + if (cutPos >= 0) { + toProcess = text.substring(0, cutPos + 1); + } + break; + case STRIP: + strip = true; + break; + case WHITESPACE: + break; + case DEFAULT: + default: + // Internal error; no translation + throw new IllegalArgumentException("Invalid clean-up mode " + mode); //$NON-NLS-1$ + } + // WHITESPACE + StringBuilder result = new StringBuilder(); + boolean lastWasEmpty = true; + for (String line : toProcess.split("\n")) { //$NON-NLS-1$ + line = line.stripTrailing(); + if (line.isEmpty()) { + if (!lastWasEmpty) { + result.append('\n'); + lastWasEmpty = true; + } + } else if (!strip || !isComment(line, commentChar)) { + lastWasEmpty = false; + result.append(line).append('\n'); + } + } + int bufferSize = result.length(); + if (lastWasEmpty && bufferSize > 0) { + bufferSize--; + result.setLength(bufferSize); + } + if (bufferSize > 0 && !toProcess.endsWith("\n")) { //$NON-NLS-1$ + if (result.charAt(bufferSize - 1) == '\n') { + result.setLength(bufferSize - 1); + } + } + return result.toString(); + } + + private static boolean isComment(String text, char commentChar) { + int len = text.length(); + for (int i = 0; i < len; i++) { + char ch = text.charAt(i); + if (!Character.isWhitespace(ch)) { + return ch == commentChar; + } + } + return false; + } } \ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 4b21e4be4..af2b4ccdd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -181,6 +181,13 @@ public final class ConfigConstants { */ public static final String CONFIG_TAG_SECTION = "tag"; + /** + * The "cleanup" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_CLEANUP = "cleanup"; + /** * The "gpgSign" key *