diff --git a/plugin/src/main/java/git4idea/history/GitHistoryUtils.java b/plugin/src/main/java/git4idea/history/GitHistoryUtils.java index 75727e4..59cce1e 100644 --- a/plugin/src/main/java/git4idea/history/GitHistoryUtils.java +++ b/plugin/src/main/java/git4idea/history/GitHistoryUtils.java @@ -15,13 +15,11 @@ */ package git4idea.history; -import consulo.application.Application; import consulo.application.util.Semaphore; import consulo.application.util.registry.Registry; import consulo.component.ProcessCanceledException; import consulo.git.localize.GitLocalize; import consulo.ide.ServiceManager; -import consulo.logging.Logger; import consulo.process.ProcessOutputTypes; import consulo.project.Project; import consulo.util.collection.ArrayUtil; @@ -59,6 +57,8 @@ import git4idea.log.GitRefManager; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -73,13 +73,13 @@ * A collection of methods for retrieving history information from native Git. */ public class GitHistoryUtils { + private static final Logger LOG = LoggerFactory.getLogger(GitHistoryUtils.class); + /** * A parameter to {@code git log} which is equivalent to {@code --all}, but doesn't show the stuff from index or stash. */ public static final List LOG_ALL = Arrays.asList("HEAD", "--branches", "--remotes", "--tags"); - private static final Logger LOG = Logger.getInstance(GitHistoryUtils.class); - private GitHistoryUtils() { } @@ -376,7 +376,7 @@ public void processTerminated(int exitCode) { } } catch (Throwable t) { - LOG.error(t); + LOG.error("Error while terminating process", t); exceptionConsumer.accept(new VcsException("Internal error " + t.getMessage(), t)); criticalFailure.set(true); } @@ -614,7 +614,9 @@ private static void processHandlerOutputByLine( if (parseError.isNull()) { parseError.set(t); LOG.error( - "Could not parse \" " + StringUtil.escapeStringCharacters(builder.toString()) + "\"\n" + "Command " + handler.printableCommandLine(), + "Could not parse \"{}\"\nCommand {}", + StringEscapeUtil.escape(builder, '"'), + handler.printableCommandLine(), t ); } @@ -731,7 +733,7 @@ private static Collection parseRefs( @Nullable private static VcsLogObjectsFactory getObjectsFactoryWithDisposeCheck(@Nonnull Project project) { - return Application.get().runReadAction((Supplier) () -> { + return project.getApplication().runReadAction((Supplier) () -> { if (!project.isDisposed()) { return ServiceManager.getService(project, VcsLogObjectsFactory.class); } @@ -901,7 +903,7 @@ record -> { for (VcsRef ref : refsInRecord) { if (!refs.add(ref)) { VcsRef otherRef = ContainerUtil.find(refs, r -> GitLogProvider.DONT_CONSIDER_SHA.equals(r, ref)); - LOG.error("Adding duplicate element " + ref + " to the set containing " + otherRef); + LOG.error("Adding duplicate element {} to the set containing {}", ref, otherRef); } } return commit; diff --git a/plugin/src/main/java/git4idea/history/GitLogParser.java b/plugin/src/main/java/git4idea/history/GitLogParser.java index 617dcee..105231d 100644 --- a/plugin/src/main/java/git4idea/history/GitLogParser.java +++ b/plugin/src/main/java/git4idea/history/GitLogParser.java @@ -16,11 +16,11 @@ package git4idea.history; import consulo.project.Project; +import consulo.util.lang.StringEscapeUtil; import consulo.util.lang.StringUtil; import git4idea.GitFormatException; import git4idea.GitVcs; import git4idea.config.GitVersionSpecialty; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -58,210 +58,230 @@ * @see GitLogRecord */ public class GitLogParser { - // Single records begin with %x01, end with %03. Items of commit information (hash, committer, subject, etc.) are separated by %x02. - // each character is declared twice - for Git pattern format and for actual character in the output. - public static final String RECORD_START = "\u0001"; - public static final String ITEMS_SEPARATOR = "\u0002"; - public static final String RECORD_END = "\u0003"; - public static final String RECORD_START_GIT = "%x01"; - private static final String ITEMS_SEPARATOR_GIT = "%x02"; - private static final String RECORD_END_GIT = "%x03"; + // Single records begin with %x01, end with %03. Items of commit information (hash, committer, subject, etc.) are separated by %x02. + // each character is declared twice - for Git pattern format and for actual character in the output. + public static final String RECORD_START = "\u0001"; + public static final String ITEMS_SEPARATOR = "\u0002"; + public static final String RECORD_END = "\u0003"; + public static final String RECORD_START_GIT = "%x01"; + private static final String ITEMS_SEPARATOR_GIT = "%x02"; + private static final String RECORD_END_GIT = "%x03"; - private final String myFormat; // pretty custom format generated in the constructor - private final GitLogOption[] myOptions; - private final boolean mySupportsRawBody; - private final NameStatus myNameStatusOption; + private final String myFormat; // pretty custom format generated in the constructor + private final GitLogOption[] myOptions; + private final boolean mySupportsRawBody; + private final NameStatus myNameStatusOption; - /** - * Record format: + /** + * Record format: * - * One git log record. - * RECORD_START - optional: it is split out when calling parse() but it is not when calling parseOneRecord() directly. - * commit information separated by ITEMS_SEPARATOR. - * RECORD_END - * Optionally: changed paths or paths with statuses (if --name-only or --name-status options are given). + * One git log record. + * RECORD_START - optional: it is split out when calling parse() but it is not when calling parseOneRecord() directly. + * commit information separated by ITEMS_SEPARATOR. + * RECORD_END + * Optionally: changed paths or paths with statuses (if --name-only or --name-status options are given). * - * Example: - * 2c815939f45fbcfda9583f84b14fe9d393ada790sample commit - * D a.txt - */ - private static final Pattern ONE_RECORD = Pattern.compile(RECORD_START + "?(.*)" + RECORD_END + "\n*(.*)", Pattern.DOTALL); - private static final String SINGLE_PATH = "([^\t\r\n]+)"; // something not empty, not a tab or newline. - private static final String EOL = "\\s*(?:\r|\n|\r\n)"; - private static final String PATHS = - SINGLE_PATH + // First path - required. - "(?:\t" + SINGLE_PATH + ")?" + // Second path - optional. Paths are separated by tab. - "(?:" + EOL + ")?"; // Path(s) information ends with a line terminator (possibly except the last path in the output). + * Example: + * 2c815939f45fbcfda9583f84b14fe9d393ada790sample commit + * D a.txt + */ + private static final Pattern ONE_RECORD = Pattern.compile(RECORD_START + "?(.*)" + RECORD_END + "\n*(.*)", Pattern.DOTALL); + private static final String SINGLE_PATH = "([^\t\r\n]+)"; // something not empty, not a tab or newline. + private static final String EOL = "\\s*(?:\r|\n|\r\n)"; + private static final String PATHS = + SINGLE_PATH + // First path - required. + "(?:\t" + SINGLE_PATH + ")?" + // Second path - optional. Paths are separated by tab. + "(?:" + EOL + ")?"; + // Path(s) information ends with a line terminator (possibly except the last path in the output). - private static Pattern NAME_ONLY = Pattern.compile(PATHS); - private static Pattern NAME_STATUS = Pattern.compile("([\\S]+)\t" + PATHS); + private static Pattern NAME_ONLY = Pattern.compile(PATHS); + private static Pattern NAME_STATUS = Pattern.compile("([\\S]+)\t" + PATHS); - // --name-only, --name-status or no flag - enum NameStatus { + // --name-only, --name-status or no flag + enum NameStatus { /** No flag. */ - NONE, + NONE, /** --name-only */ - NAME, + NAME, /** --name-status */ - STATUS - } + STATUS + } + + /** + * Options which may be passed to 'git log --pretty=format:' as placeholders and then parsed from the result. + * These are the pieces of information about a commit which we want to get from 'git log'. + */ + enum GitLogOption { + HASH("H"), + COMMIT_TIME("ct"), + AUTHOR_NAME("an"), + AUTHOR_TIME("at"), + AUTHOR_EMAIL("ae"), + COMMITTER_NAME("cn"), + COMMITTER_EMAIL("ce"), + SUBJECT("s"), + BODY("b"), + PARENTS("P"), + REF_NAMES("d"), + SHORT_REF_LOG_SELECTOR("gd"), + RAW_BODY("B"); - /** - * Options which may be passed to 'git log --pretty=format:' as placeholders and then parsed from the result. - * These are the pieces of information about a commit which we want to get from 'git log'. - */ - enum GitLogOption { - HASH("H"), COMMIT_TIME("ct"), AUTHOR_NAME("an"), AUTHOR_TIME("at"), AUTHOR_EMAIL("ae"), COMMITTER_NAME("cn"), - COMMITTER_EMAIL("ce"), SUBJECT("s"), BODY("b"), PARENTS("P"), REF_NAMES("d"), SHORT_REF_LOG_SELECTOR("gd"), - RAW_BODY("B"); + private String myPlaceholder; - private String myPlaceholder; - GitLogOption(String placeholder) { myPlaceholder = placeholder; } - private String getPlaceholder() { return myPlaceholder; } - } + GitLogOption(String placeholder) { + myPlaceholder = placeholder; + } - /** - * Constructs new parser with the given options and no names of changed files in the output. - */ - GitLogParser(Project project, GitLogOption... options) { - this(project, NameStatus.NONE, options); - } + private String getPlaceholder() { + return myPlaceholder; + } + } - /** - * Constructs new parser with the specified options. - * Only these options will be parsed out and thus will be available from the GitLogRecord. - */ - GitLogParser(Project project, NameStatus nameStatusOption, GitLogOption... options) { - myFormat = makeFormatFromOptions(options); - myOptions = options; - myNameStatusOption = nameStatusOption; - GitVcs vcs = GitVcs.getInstance(project); - mySupportsRawBody = vcs != null && GitVersionSpecialty.STARTED_USING_RAW_BODY_IN_FORMAT.existsIn(vcs.getVersion()); - } + /** + * Constructs new parser with the given options and no names of changed files in the output. + */ + GitLogParser(Project project, GitLogOption... options) { + this(project, NameStatus.NONE, options); + } - private static String makeFormatFromOptions(GitLogOption[] options) { - Function function = option -> "%" + option.getPlaceholder(); - return RECORD_START_GIT + StringUtil.join(options, function, ITEMS_SEPARATOR_GIT) + RECORD_END_GIT; - } + /** + * Constructs new parser with the specified options. + * Only these options will be parsed out and thus will be available from the GitLogRecord. + */ + GitLogParser(Project project, NameStatus nameStatusOption, GitLogOption... options) { + myFormat = makeFormatFromOptions(options); + myOptions = options; + myNameStatusOption = nameStatusOption; + GitVcs vcs = GitVcs.getInstance(project); + mySupportsRawBody = vcs != null && GitVersionSpecialty.STARTED_USING_RAW_BODY_IN_FORMAT.existsIn(vcs.getVersion()); + } - String getPretty() { - return "--pretty=format:" + myFormat; - } + private static String makeFormatFromOptions(GitLogOption[] options) { + Function function = option -> "%" + option.getPlaceholder(); + return RECORD_START_GIT + StringUtil.join(options, function, ITEMS_SEPARATOR_GIT) + RECORD_END_GIT; + } - /** - * Parses the output returned from 'git log' which was executed with '--pretty=format:' pattern retrieved from {@link #getPretty()}. - * @param output 'git log' output to be parsed. - * @return The list of {@link GitLogRecord GitLogRecords} with information for each revision. - * The list is sorted as usual for git log - the first is the newest, the last is the oldest. - */ - @Nonnull - List parse(@Nonnull String output) { - // Here is what git log returns for --pretty=tformat:^%H#%s$ - // ^2c815939f45fbcfda9583f84b14fe9d393ada790#sample commit$ - // - // D a.txt - // ^b71477e9738168aa67a8d41c414f284255f81e8a#moved out$ - // - // R100 dir/anew.txt anew.txt - final String[] records = output.split(RECORD_START); // split by START, because END is the end of information, but not the end of the record: file status and path follow. - final List res = new ArrayList(records.length); - for (String record : records) { - if (!record.trim().isEmpty()) { // record[0] is empty for sure, because we're splitting on RECORD_START. Just to play safe adding the check for all records. - res.add(parseOneRecord(record)); - } - } - return res; - } + String getPretty() { + return "--pretty=format:" + myFormat; + } - /** - * Parses a single record returned by 'git log'. The record contains information from pattern and file status and path (if respective - * flags --name-only or name-status were provided). - * @param line record to be parsed. - * @return GitLogRecord with information about the revision or {@code null} if the given line is empty. - * @throws GitFormatException if the line is given in unexpected format. - */ - @Nullable - GitLogRecord parseOneRecord(@Nonnull String line) { - if (line.isEmpty()) { - return null; - } - Matcher matcher = ONE_RECORD.matcher(line); - if (!matcher.matches()) { - throwGFE("ONE_RECORD didn't match", line); - } - String commitInfo = matcher.group(1); - if (commitInfo == null) { - throwGFE("No match for group#1 in", line); - } + /** + * Parses the output returned from 'git log' which was executed with '--pretty=format:' pattern retrieved from {@link #getPretty()}. + * + * @param output 'git log' output to be parsed. + * @return The list of {@link GitLogRecord GitLogRecords} with information for each revision. + * The list is sorted as usual for git log - the first is the newest, the last is the oldest. + */ + @Nonnull + List parse(@Nonnull String output) { + // Here is what git log returns for --pretty=tformat:^%H#%s$ + // ^2c815939f45fbcfda9583f84b14fe9d393ada790#sample commit$ + // + // D a.txt + // ^b71477e9738168aa67a8d41c414f284255f81e8a#moved out$ + // + // R100 dir/anew.txt anew.txt + String[] records = + output.split(RECORD_START); // split by START, because END is the end of information, but not the end of the record: file status and path follow. + List res = new ArrayList<>(records.length); + for (String record : records) { + if (!record.trim() + .isEmpty()) { // record[0] is empty for sure, because we're splitting on RECORD_START. Just to play safe adding the check for all records. + res.add(parseOneRecord(record)); + } + } + return res; + } - final Map res = parseCommitInfo(commitInfo); + /** + * Parses a single record returned by 'git log'. The record contains information from pattern and file status and path (if respective + * flags --name-only or name-status were provided). + * + * @param line record to be parsed. + * @return GitLogRecord with information about the revision or {@code null} if the given line is empty. + * @throws GitFormatException if the line is given in unexpected format. + */ + @Nullable + GitLogRecord parseOneRecord(@Nonnull String line) { + if (line.isEmpty()) { + return null; + } + Matcher matcher = ONE_RECORD.matcher(line); + if (!matcher.matches()) { + throwGFE("ONE_RECORD didn't match", line); + } + String commitInfo = matcher.group(1); + if (commitInfo == null) { + throwGFE("No match for group#1 in", line); + } - // parsing status and path (if given) - final List paths = new ArrayList(1); - final List statuses = new ArrayList(); + Map res = parseCommitInfo(commitInfo); - if (myNameStatusOption != NameStatus.NONE) { - String pathsAndStatuses = matcher.group(2); - if (pathsAndStatuses == null) { - throwGFE("No match for group#2 in", line); - } + // parsing status and path (if given) + List paths = new ArrayList<>(1); + List statuses = new ArrayList<>(); - if (myNameStatusOption == NameStatus.NAME) { - Matcher pathsMatcher = NAME_ONLY.matcher(pathsAndStatuses); - while (pathsMatcher.find()) { - String path1 = pathsMatcher.group(1); - String path2 = pathsMatcher.group(2); - assertNotNull(path1, "path", pathsAndStatuses); - paths.add(path1); - if (path2 != null) { // null is perfectly legal here: second path is given only in case of rename - paths.add(path2); - } - } - } - else { - Matcher nameStatusMatcher = NAME_STATUS.matcher(pathsAndStatuses); - while (nameStatusMatcher.find()) { - String status = nameStatusMatcher.group(1); - String path1 = nameStatusMatcher.group(2); - String path2 = nameStatusMatcher.group(3); - assertNotNull(status, "status", pathsAndStatuses); - assertNotNull(path1, "path1", pathsAndStatuses); - paths.add(path1); - if (path2 != null) { - paths.add(path2); - } - statuses.add(new GitLogStatusInfo(GitChangeType.fromString(status), path1, path2)); - } - } - } - return new GitLogRecord(res, paths, statuses, mySupportsRawBody); - } + if (myNameStatusOption != NameStatus.NONE) { + String pathsAndStatuses = matcher.group(2); + if (pathsAndStatuses == null) { + throwGFE("No match for group#2 in", line); + } + if (myNameStatusOption == NameStatus.NAME) { + Matcher pathsMatcher = NAME_ONLY.matcher(pathsAndStatuses); + while (pathsMatcher.find()) { + String path1 = pathsMatcher.group(1); + String path2 = pathsMatcher.group(2); + assertNotNull(path1, "path", pathsAndStatuses); + paths.add(path1); + if (path2 != null) { // null is perfectly legal here: second path is given only in case of rename + paths.add(path2); + } + } + } + else { + Matcher nameStatusMatcher = NAME_STATUS.matcher(pathsAndStatuses); + while (nameStatusMatcher.find()) { + String status = nameStatusMatcher.group(1); + String path1 = nameStatusMatcher.group(2); + String path2 = nameStatusMatcher.group(3); + assertNotNull(status, "status", pathsAndStatuses); + assertNotNull(path1, "path1", pathsAndStatuses); + paths.add(path1); + if (path2 != null) { + paths.add(path2); + } + statuses.add(new GitLogStatusInfo(GitChangeType.fromString(status), path1, path2)); + } + } + } + return new GitLogRecord(res, paths, statuses, mySupportsRawBody); + } - @Nonnull - private Map parseCommitInfo(@Nonnull String commitInfo) { - // parsing revision information - // we rely on the order of options - final String[] values = commitInfo.split(ITEMS_SEPARATOR); - final Map res = new HashMap(values.length); - int i = 0; - for (; i < values.length && i < myOptions.length; i++) { // fill valid values - res.put(myOptions[i], values[i]); - } - for (; i < myOptions.length; i++) { // options which were not returned are set to blank string, extra options are ignored. - res.put(myOptions[i], ""); - } - return res; - } - private static void assertNotNull(String value, String valueName, String line) { - if (value == null) { - throwGFE("Unexpectedly null " + valueName + " in ", line); - } - } + @Nonnull + private Map parseCommitInfo(@Nonnull String commitInfo) { + // parsing revision information + // we rely on the order of options + String[] values = commitInfo.split(ITEMS_SEPARATOR); + Map res = new HashMap<>(values.length); + int i = 0; + for (; i < values.length && i < myOptions.length; i++) { // fill valid values + res.put(myOptions[i], values[i]); + } + for (; i < myOptions.length; i++) { // options which were not returned are set to blank string, extra options are ignored. + res.put(myOptions[i], ""); + } + return res; + } - private static void throwGFE(String message, String line) { - throw new GitFormatException(message + " [" + StringUtil.escapeStringCharacters(line) + "]"); - } + private static void assertNotNull(String value, String valueName, String line) { + if (value == null) { + throwGFE("Unexpectedly null " + valueName + " in ", line); + } + } + private static void throwGFE(String message, String line) { + throw new GitFormatException(message + " [" + StringEscapeUtil.escape(line, '"') + "]"); + } }