| /* |
| * Copyright (c) 2020, 2022 Julian Ruppel <julian.ruppel@sap.com> 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 java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.nio.charset.IllegalCharsetNameException; |
| 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; |
| import org.eclipse.jgit.util.RawParseUtils; |
| import org.eclipse.jgit.util.StringUtils; |
| |
| /** |
| * The standard "commit" configuration parameters. |
| * |
| * @since 5.13 |
| */ |
| public class CommitConfig { |
| |
| /** |
| * Key for {@link Config#get(SectionParser)}. |
| */ |
| public static final Config.SectionParser<CommitConfig> KEY = CommitConfig::new; |
| |
| private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$ |
| |
| private static final char[] COMMENT_CHARS = { '#', ';', '@', '!', '$', '%', |
| '^', '&', '|', ':' }; |
| |
| /** |
| * 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 char commentCharacter = '#'; |
| |
| private boolean autoCommentChar = false; |
| |
| 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); |
| String comment = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null, |
| ConfigConstants.CONFIG_KEY_COMMENT_CHAR); |
| if (!StringUtils.isEmptyOrNull(comment)) { |
| if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$ |
| autoCommentChar = true; |
| } else { |
| char first = comment.charAt(0); |
| if (first > ' ' && first < 127) { |
| commentCharacter = first; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Get the path to the commit template as defined in the git |
| * {@code commit.template} property. |
| * |
| * @return the path to commit template or {@code null} if not present. |
| */ |
| @Nullable |
| public String getCommitTemplatePath() { |
| return commitTemplatePath; |
| } |
| |
| /** |
| * Get the encoding of the commit as defined in the git |
| * {@code i18n.commitEncoding} property. |
| * |
| * @return the encoding or {@code null} if not present. |
| */ |
| @Nullable |
| public String getCommitEncoding() { |
| return i18nCommitEncoding; |
| } |
| |
| /** |
| * Retrieves the comment character set by git config |
| * {@code core.commentChar}. |
| * |
| * @return the character to use for comments in commit messages |
| * @since 6.2 |
| */ |
| public char getCommentChar() { |
| return commentCharacter; |
| } |
| |
| /** |
| * Determines the comment character to use for a particular text. If |
| * {@code core.commentChar} is "auto", tries to determine an unused |
| * character; if none is found, falls back to '#'. Otherwise returns the |
| * character given by {@code core.commentChar}. |
| * |
| * @param text |
| * existing text |
| * |
| * @return the character to use |
| * @since 6.2 |
| */ |
| public char getCommentChar(String text) { |
| if (isAutoCommentChar()) { |
| char toUse = determineCommentChar(text); |
| if (toUse > 0) { |
| return toUse; |
| } |
| return '#'; |
| } |
| return getCommentChar(); |
| } |
| |
| /** |
| * Tells whether the comment character should be determined by choosing a |
| * character not occurring in a commit message. |
| * |
| * @return {@code true} if git config {@code core.commentChar} is "auto" |
| * @since 6.2 |
| */ |
| public boolean isAutoCommentChar() { |
| return autoCommentChar; |
| } |
| |
| /** |
| * 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, |
| * UTF-8 fallback is used. |
| * |
| * @param repository |
| * to resolve relative path in local git repo config |
| * |
| * @return content of the commit template or {@code null} if not present. |
| * @throws IOException |
| * if the template file can not be read |
| * @throws FileNotFoundException |
| * if the template file does not exists |
| * @throws ConfigInvalidException |
| * if a {@code commitEncoding} is specified and is invalid |
| * @since 6.0 |
| */ |
| @Nullable |
| public String getCommitTemplateContent(@NonNull Repository repository) |
| throws FileNotFoundException, IOException, ConfigInvalidException { |
| |
| if (commitTemplatePath == null) { |
| return null; |
| } |
| |
| File commitTemplateFile; |
| FS fileSystem = repository.getFS(); |
| if (commitTemplatePath.startsWith("~/")) { //$NON-NLS-1$ |
| commitTemplateFile = fileSystem.resolve(fileSystem.userHome(), |
| commitTemplatePath.substring(2)); |
| } else { |
| commitTemplateFile = fileSystem.resolve(null, commitTemplatePath); |
| } |
| if (!commitTemplateFile.isAbsolute()) { |
| commitTemplateFile = fileSystem.resolve( |
| repository.getWorkTree().getAbsoluteFile(), |
| commitTemplatePath); |
| } |
| |
| Charset commitMessageEncoding = getEncoding(); |
| return RawParseUtils.decode(commitMessageEncoding, |
| IO.readFully(commitTemplateFile)); |
| |
| } |
| |
| private Charset getEncoding() throws ConfigInvalidException { |
| Charset commitMessageEncoding = DEFAULT_COMMIT_MESSAGE_ENCODING; |
| |
| if (i18nCommitEncoding == null) { |
| return null; |
| } |
| |
| try { |
| commitMessageEncoding = Charset.forName(i18nCommitEncoding); |
| } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { |
| throw new ConfigInvalidException(MessageFormat.format( |
| JGitText.get().invalidEncoding, i18nCommitEncoding), e); |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Determines a comment character by choosing one from a limited set of |
| * 7-bit ASCII characters that do not occur in the given text at the |
| * beginning of any line. If none can be determined, {@code (char) 0} is |
| * returned. |
| * |
| * @param text |
| * to get a comment character for |
| * @return the comment character, or {@code (char) 0} if none could be |
| * determined |
| * @since 6.2 |
| */ |
| public static char determineCommentChar(String text) { |
| if (StringUtils.isEmptyOrNull(text)) { |
| return '#'; |
| } |
| final boolean[] inUse = new boolean[127]; |
| for (String line : text.split("\n")) { //$NON-NLS-1$ |
| int len = line.length(); |
| for (int i = 0; i < len; i++) { |
| char ch = line.charAt(i); |
| if (!Character.isWhitespace(ch)) { |
| if (ch < inUse.length) { |
| inUse[ch] = true; |
| } |
| break; |
| } |
| } |
| } |
| for (char candidate : COMMENT_CHARS) { |
| if (!inUse[candidate]) { |
| return candidate; |
| } |
| } |
| return (char) 0; |
| } |
| } |