CommitConfig.java

  1. /*
  2.  * Copyright (c) 2020, 2022 Julian Ruppel <julian.ruppel@sap.com> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.lib;

  11. import java.io.File;
  12. import java.io.FileNotFoundException;
  13. import java.io.IOException;
  14. import java.nio.charset.Charset;
  15. import java.nio.charset.IllegalCharsetNameException;
  16. import java.nio.charset.StandardCharsets;
  17. import java.nio.charset.UnsupportedCharsetException;
  18. import java.text.MessageFormat;
  19. import java.util.Locale;

  20. import org.eclipse.jgit.annotations.NonNull;
  21. import org.eclipse.jgit.annotations.Nullable;
  22. import org.eclipse.jgit.errors.ConfigInvalidException;
  23. import org.eclipse.jgit.internal.JGitText;
  24. import org.eclipse.jgit.lib.Config.ConfigEnum;
  25. import org.eclipse.jgit.lib.Config.SectionParser;
  26. import org.eclipse.jgit.util.FS;
  27. import org.eclipse.jgit.util.IO;
  28. import org.eclipse.jgit.util.RawParseUtils;
  29. import org.eclipse.jgit.util.StringUtils;

  30. /**
  31.  * The standard "commit" configuration parameters.
  32.  *
  33.  * @since 5.13
  34.  */
  35. public class CommitConfig {

  36.     /**
  37.      * Key for {@link Config#get(SectionParser)}.
  38.      */
  39.     public static final Config.SectionParser<CommitConfig> KEY = CommitConfig::new;

  40.     private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$

  41.     private static final char[] COMMENT_CHARS = { '#', ';', '@', '!', '$', '%',
  42.             '^', '&', '|', ':' };

  43.     /**
  44.      * How to clean up commit messages when committing.
  45.      *
  46.      * @since 6.1
  47.      */
  48.     public enum CleanupMode implements ConfigEnum {

  49.         /**
  50.          * {@link #WHITESPACE}, additionally remove comment lines.
  51.          */
  52.         STRIP,

  53.         /**
  54.          * Remove trailing whitespace and leading and trailing empty lines;
  55.          * collapse multiple empty lines to a single one.
  56.          */
  57.         WHITESPACE,

  58.         /**
  59.          * Make no changes.
  60.          */
  61.         VERBATIM,

  62.         /**
  63.          * Omit everything from the first "scissor" line on, then apply
  64.          * {@link #WHITESPACE}.
  65.          */
  66.         SCISSORS,

  67.         /**
  68.          * Use {@link #STRIP} for user-edited messages, otherwise
  69.          * {@link #WHITESPACE}, unless overridden by a git config setting other
  70.          * than DEFAULT.
  71.          */
  72.         DEFAULT;

  73.         @Override
  74.         public String toConfigValue() {
  75.             return name().toLowerCase(Locale.ROOT);
  76.         }

  77.         @Override
  78.         public boolean matchConfigValue(String in) {
  79.             return toConfigValue().equals(in);
  80.         }
  81.     }

  82.     private final static Charset DEFAULT_COMMIT_MESSAGE_ENCODING = StandardCharsets.UTF_8;

  83.     private String i18nCommitEncoding;

  84.     private String commitTemplatePath;

  85.     private CleanupMode cleanupMode;

  86.     private char commentCharacter = '#';

  87.     private boolean autoCommentChar = false;

  88.     private CommitConfig(Config rc) {
  89.         commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION,
  90.                 null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE);
  91.         i18nCommitEncoding = rc.getString(ConfigConstants.CONFIG_SECTION_I18N,
  92.                 null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING);
  93.         cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null,
  94.                 ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT);
  95.         String comment = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
  96.                 ConfigConstants.CONFIG_KEY_COMMENT_CHAR);
  97.         if (!StringUtils.isEmptyOrNull(comment)) {
  98.             if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$
  99.                 autoCommentChar = true;
  100.             } else {
  101.                 char first = comment.charAt(0);
  102.                 if (first > ' ' && first < 127) {
  103.                     commentCharacter = first;
  104.                 }
  105.             }
  106.         }
  107.     }

  108.     /**
  109.      * Get the path to the commit template as defined in the git
  110.      * {@code commit.template} property.
  111.      *
  112.      * @return the path to commit template or {@code null} if not present.
  113.      */
  114.     @Nullable
  115.     public String getCommitTemplatePath() {
  116.         return commitTemplatePath;
  117.     }

  118.     /**
  119.      * Get the encoding of the commit as defined in the git
  120.      * {@code i18n.commitEncoding} property.
  121.      *
  122.      * @return the encoding or {@code null} if not present.
  123.      */
  124.     @Nullable
  125.     public String getCommitEncoding() {
  126.         return i18nCommitEncoding;
  127.     }

  128.     /**
  129.      * Retrieves the comment character set by git config
  130.      * {@code core.commentChar}.
  131.      *
  132.      * @return the character to use for comments in commit messages
  133.      * @since 6.2
  134.      */
  135.     public char getCommentChar() {
  136.         return commentCharacter;
  137.     }

  138.     /**
  139.      * Determines the comment character to use for a particular text. If
  140.      * {@code core.commentChar} is "auto", tries to determine an unused
  141.      * character; if none is found, falls back to '#'. Otherwise returns the
  142.      * character given by {@code core.commentChar}.
  143.      *
  144.      * @param text
  145.      *            existing text
  146.      *
  147.      * @return the character to use
  148.      * @since 6.2
  149.      */
  150.     public char getCommentChar(String text) {
  151.         if (isAutoCommentChar()) {
  152.             char toUse = determineCommentChar(text);
  153.             if (toUse > 0) {
  154.                 return toUse;
  155.             }
  156.             return '#';
  157.         }
  158.         return getCommentChar();
  159.     }

  160.     /**
  161.      * Tells whether the comment character should be determined by choosing a
  162.      * character not occurring in a commit message.
  163.      *
  164.      * @return {@code true} if git config {@code core.commentChar} is "auto"
  165.      * @since 6.2
  166.      */
  167.     public boolean isAutoCommentChar() {
  168.         return autoCommentChar;
  169.     }

  170.     /**
  171.      * Retrieves the {@link CleanupMode} as given by git config
  172.      * {@code commit.cleanup}.
  173.      *
  174.      * @return the {@link CleanupMode}; {@link CleanupMode#DEFAULT} if the git
  175.      *         config is not set
  176.      * @since 6.1
  177.      */
  178.     @NonNull
  179.     public CleanupMode getCleanupMode() {
  180.         return cleanupMode;
  181.     }

  182.     /**
  183.      * Computes a non-default {@link CleanupMode} from the given mode and the
  184.      * git config.
  185.      *
  186.      * @param mode
  187.      *            {@link CleanupMode} to resolve
  188.      * @param defaultStrip
  189.      *            if {@code true} return {@link CleanupMode#STRIP} if the git
  190.      *            config is also "default", otherwise return
  191.      *            {@link CleanupMode#WHITESPACE}
  192.      * @return the {@code mode}, if it is not {@link CleanupMode#DEFAULT},
  193.      *         otherwise the resolved mode, which is never
  194.      *         {@link CleanupMode#DEFAULT}
  195.      * @since 6.1
  196.      */
  197.     @NonNull
  198.     public CleanupMode resolve(@NonNull CleanupMode mode,
  199.             boolean defaultStrip) {
  200.         if (CleanupMode.DEFAULT == mode) {
  201.             CleanupMode defaultMode = getCleanupMode();
  202.             if (CleanupMode.DEFAULT == defaultMode) {
  203.                 return defaultStrip ? CleanupMode.STRIP
  204.                         : CleanupMode.WHITESPACE;
  205.             }
  206.             return defaultMode;
  207.         }
  208.         return mode;
  209.     }

  210.     /**
  211.      * Get the content to the commit template as defined in
  212.      * {@code commit.template}. If no {@code i18n.commitEncoding} is specified,
  213.      * UTF-8 fallback is used.
  214.      *
  215.      * @param repository
  216.      *            to resolve relative path in local git repo config
  217.      *
  218.      * @return content of the commit template or {@code null} if not present.
  219.      * @throws IOException
  220.      *             if the template file can not be read
  221.      * @throws FileNotFoundException
  222.      *             if the template file does not exists
  223.      * @throws ConfigInvalidException
  224.      *             if a {@code commitEncoding} is specified and is invalid
  225.      * @since 6.0
  226.      */
  227.     @Nullable
  228.     public String getCommitTemplateContent(@NonNull Repository repository)
  229.             throws FileNotFoundException, IOException, ConfigInvalidException {

  230.         if (commitTemplatePath == null) {
  231.             return null;
  232.         }

  233.         File commitTemplateFile;
  234.         FS fileSystem = repository.getFS();
  235.         if (commitTemplatePath.startsWith("~/")) { //$NON-NLS-1$
  236.             commitTemplateFile = fileSystem.resolve(fileSystem.userHome(),
  237.                     commitTemplatePath.substring(2));
  238.         } else {
  239.             commitTemplateFile = fileSystem.resolve(null, commitTemplatePath);
  240.         }
  241.         if (!commitTemplateFile.isAbsolute()) {
  242.             commitTemplateFile = fileSystem.resolve(
  243.                     repository.getWorkTree().getAbsoluteFile(),
  244.                     commitTemplatePath);
  245.         }

  246.         Charset commitMessageEncoding = getEncoding();
  247.         return RawParseUtils.decode(commitMessageEncoding,
  248.                 IO.readFully(commitTemplateFile));

  249.     }

  250.     private Charset getEncoding() throws ConfigInvalidException {
  251.         Charset commitMessageEncoding = DEFAULT_COMMIT_MESSAGE_ENCODING;

  252.         if (i18nCommitEncoding == null) {
  253.             return null;
  254.         }

  255.         try {
  256.             commitMessageEncoding = Charset.forName(i18nCommitEncoding);
  257.         } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
  258.             throw new ConfigInvalidException(MessageFormat.format(
  259.                     JGitText.get().invalidEncoding, i18nCommitEncoding), e);
  260.         }

  261.         return commitMessageEncoding;
  262.     }

  263.     /**
  264.      * Processes a text according to the given {@link CleanupMode}.
  265.      *
  266.      * @param text
  267.      *            text to process
  268.      * @param mode
  269.      *            {@link CleanupMode} to use
  270.      * @param commentChar
  271.      *            comment character (normally {@code #}) to use if {@code mode}
  272.      *            is {@link CleanupMode#STRIP} or {@link CleanupMode#SCISSORS}
  273.      * @return the processed text
  274.      * @throws IllegalArgumentException
  275.      *             if {@code mode} is {@link CleanupMode#DEFAULT} (use
  276.      *             {@link #resolve(CleanupMode, boolean)} first)
  277.      * @since 6.1
  278.      */
  279.     public static String cleanText(@NonNull String text,
  280.             @NonNull CleanupMode mode, char commentChar) {
  281.         String toProcess = text;
  282.         boolean strip = false;
  283.         switch (mode) {
  284.         case VERBATIM:
  285.             return text;
  286.         case SCISSORS:
  287.             String cut = commentChar + CUT;
  288.             if (text.startsWith(cut)) {
  289.                 return ""; //$NON-NLS-1$
  290.             }
  291.             int cutPos = text.indexOf('\n' + cut);
  292.             if (cutPos >= 0) {
  293.                 toProcess = text.substring(0, cutPos + 1);
  294.             }
  295.             break;
  296.         case STRIP:
  297.             strip = true;
  298.             break;
  299.         case WHITESPACE:
  300.             break;
  301.         case DEFAULT:
  302.         default:
  303.             // Internal error; no translation
  304.             throw new IllegalArgumentException("Invalid clean-up mode " + mode); //$NON-NLS-1$
  305.         }
  306.         // WHITESPACE
  307.         StringBuilder result = new StringBuilder();
  308.         boolean lastWasEmpty = true;
  309.         for (String line : toProcess.split("\n")) { //$NON-NLS-1$
  310.             line = line.stripTrailing();
  311.             if (line.isEmpty()) {
  312.                 if (!lastWasEmpty) {
  313.                     result.append('\n');
  314.                     lastWasEmpty = true;
  315.                 }
  316.             } else if (!strip || !isComment(line, commentChar)) {
  317.                 lastWasEmpty = false;
  318.                 result.append(line).append('\n');
  319.             }
  320.         }
  321.         int bufferSize = result.length();
  322.         if (lastWasEmpty && bufferSize > 0) {
  323.             bufferSize--;
  324.             result.setLength(bufferSize);
  325.         }
  326.         if (bufferSize > 0 && !toProcess.endsWith("\n")) { //$NON-NLS-1$
  327.             if (result.charAt(bufferSize - 1) == '\n') {
  328.                 result.setLength(bufferSize - 1);
  329.             }
  330.         }
  331.         return result.toString();
  332.     }

  333.     private static boolean isComment(String text, char commentChar) {
  334.         int len = text.length();
  335.         for (int i = 0; i < len; i++) {
  336.             char ch = text.charAt(i);
  337.             if (!Character.isWhitespace(ch)) {
  338.                 return ch == commentChar;
  339.             }
  340.         }
  341.         return false;
  342.     }

  343.     /**
  344.      * Determines a comment character by choosing one from a limited set of
  345.      * 7-bit ASCII characters that do not occur in the given text at the
  346.      * beginning of any line. If none can be determined, {@code (char) 0} is
  347.      * returned.
  348.      *
  349.      * @param text
  350.      *            to get a comment character for
  351.      * @return the comment character, or {@code (char) 0} if none could be
  352.      *         determined
  353.      * @since 6.2
  354.      */
  355.     public static char determineCommentChar(String text) {
  356.         if (StringUtils.isEmptyOrNull(text)) {
  357.             return '#';
  358.         }
  359.         final boolean[] inUse = new boolean[127];
  360.         for (String line : text.split("\n")) { //$NON-NLS-1$
  361.             int len = line.length();
  362.             for (int i = 0; i < len; i++) {
  363.                 char ch = line.charAt(i);
  364.                 if (!Character.isWhitespace(ch)) {
  365.                     if (ch >= 0 && ch < inUse.length) {
  366.                         inUse[ch] = true;
  367.                     }
  368.                     break;
  369.                 }
  370.             }
  371.         }
  372.         for (char candidate : COMMENT_CHARS) {
  373.             if (!inUse[candidate]) {
  374.                 return candidate;
  375.             }
  376.         }
  377.         return (char) 0;
  378.     }
  379. }