/* * Copyright (c) 2020 Julian Ruppel * * 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; /** * The standard "commit" configuration parameters. * * @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); } /** * 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 {@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; } }