mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-122670 Hg log: request only necessary log information
*common hg parser for log command created; *construct appropriate VcsCommit type using this HgBaseLogParser requesting only needed arguments from hg; *HgLogCommand changed; *annotations added; *String.split method calls changed to StringUtil.split if possible; *several parsing methods became package private and appropriate test moved to appropriate test package for getting access
This commit is contained in:
@@ -14,45 +14,31 @@ package org.zmlx.hg4idea.command;
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.FilePath;
|
||||
import com.intellij.openapi.vcs.changes.ChangeListManager;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.zmlx.hg4idea.HgFile;
|
||||
import org.zmlx.hg4idea.HgFileRevision;
|
||||
import org.zmlx.hg4idea.HgRevisionNumber;
|
||||
import org.zmlx.hg4idea.HgVcs;
|
||||
import org.zmlx.hg4idea.execution.HgCommandException;
|
||||
import org.zmlx.hg4idea.execution.HgCommandExecutor;
|
||||
import org.zmlx.hg4idea.execution.HgCommandResult;
|
||||
import org.zmlx.hg4idea.log.HgBaseLogParser;
|
||||
import org.zmlx.hg4idea.log.HgFileRevisionLogParser;
|
||||
import org.zmlx.hg4idea.log.HgHistoryUtil;
|
||||
import org.zmlx.hg4idea.util.HgChangesetUtil;
|
||||
import org.zmlx.hg4idea.util.HgUtil;
|
||||
import org.zmlx.hg4idea.util.HgVersion;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class HgLogCommand {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(HgLogCommand.class.getName());
|
||||
private static final int REVISION_INDEX = 0;
|
||||
private static final int CHANGESET_INDEX = 1;
|
||||
private static final int PARENTS_INDEX = 2;
|
||||
|
||||
private static final int DATE_INDEX = 3;
|
||||
private static final int AUTHOR_INDEX = 4;
|
||||
private static final int BRANCH_INDEX = 5;
|
||||
private static final int MESSAGE_INDEX = 6;
|
||||
private static final int FILES_ADDED_INDEX = 7;
|
||||
private static final int FILES_MODIFIED_INDEX = 8;
|
||||
private static final int FILES_DELETED_INDEX = 9;
|
||||
private static final int FILES_COPIED_INDEX = 10;
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
||||
|
||||
@NotNull private final Project myProject;
|
||||
@NotNull private HgVersion myVersion;
|
||||
@@ -100,107 +86,14 @@ public class HgLogCommand {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String[] templates = constructTemplateArgument(includeFiles, myVersion);
|
||||
String[] templates = HgBaseLogParser.constructFullTemplateArgument(includeFiles, myVersion);
|
||||
String template = HgChangesetUtil.makeTemplate(templates);
|
||||
int expectedItemCount = templates.length;
|
||||
boolean shouldParseOldTemplate = !myVersion.isBuiltInFunctionSupported();
|
||||
|
||||
FilePath originalFileName = HgUtil.getOriginalFileName(hgFile.toFilePath(), ChangeListManager.getInstance(myProject));
|
||||
HgFile originalHgFile = new HgFile(hgFile.getRepo(), originalFileName);
|
||||
HgCommandResult result = execute(hgFile.getRepo(), template, limit, originalHgFile, argsForCmd);
|
||||
|
||||
final List<HgFileRevision> revisions = new LinkedList<HgFileRevision>();
|
||||
if (result == null) {
|
||||
return revisions;
|
||||
}
|
||||
|
||||
List<String> errors = result.getErrorLines();
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
if (result.getExitValue() != 0) {
|
||||
throw new HgCommandException(errors.toString());
|
||||
}
|
||||
LOG.warn(errors.toString());
|
||||
}
|
||||
String output = result.getRawOutput();
|
||||
String[] changeSets = output.split(HgChangesetUtil.CHANGESET_SEPARATOR);
|
||||
for (String line : changeSets) {
|
||||
try {
|
||||
// we need to get all attributes, include empty trailing strings, so use non-positive limit as second argument
|
||||
String[] attributes = line.split(HgChangesetUtil.ITEM_SEPARATOR, -1);
|
||||
// At least in the case of the long template, it's OK that we don't have everything...for example, if there were no
|
||||
// deleted or copied files, then we won't get any attributes for them...
|
||||
int numAttributes = attributes.length;
|
||||
if (!includeFiles && (numAttributes < expectedItemCount)) { //there are may be empty string as last split attributes
|
||||
LOG.debug("Wrong format. Skipping line " + line);
|
||||
continue;
|
||||
}
|
||||
else if (includeFiles && (numAttributes < FILES_ADDED_INDEX)) {
|
||||
LOG.debug("Wrong format for long template. Skipping line " + line);
|
||||
continue;
|
||||
}
|
||||
|
||||
String revisionString = attributes[REVISION_INDEX];
|
||||
String changeset = attributes[CHANGESET_INDEX];
|
||||
String parentsString = attributes[PARENTS_INDEX];
|
||||
|
||||
List<HgRevisionNumber> parents = parseParentRevisions(parentsString, revisionString);
|
||||
|
||||
Date revisionDate = DATE_FORMAT.parse(attributes[DATE_INDEX]);
|
||||
String author = attributes[AUTHOR_INDEX];
|
||||
String branchName = attributes[BRANCH_INDEX];
|
||||
String commitMessage = attributes[MESSAGE_INDEX];
|
||||
|
||||
final HgRevisionNumber vcsRevisionNumber = new HgRevisionNumber(revisionString, changeset, author, commitMessage, parents);
|
||||
|
||||
Set<String> filesAdded = Collections.emptySet();
|
||||
Set<String> filesModified = Collections.emptySet();
|
||||
Set<String> filesDeleted = Collections.emptySet();
|
||||
Map<String, String> copies = Collections.emptyMap();
|
||||
|
||||
if (numAttributes > FILES_ADDED_INDEX) {
|
||||
filesAdded = parseFileList(attributes[FILES_ADDED_INDEX]);
|
||||
|
||||
if (numAttributes > FILES_MODIFIED_INDEX) {
|
||||
filesModified = parseFileList(attributes[FILES_MODIFIED_INDEX]);
|
||||
|
||||
if (numAttributes > FILES_DELETED_INDEX) {
|
||||
filesDeleted = parseFileList(attributes[FILES_DELETED_INDEX]);
|
||||
|
||||
if (numAttributes > FILES_COPIED_INDEX) {
|
||||
copies = shouldParseOldTemplate
|
||||
? parseCopiesFileListAsOldVersion(attributes[FILES_COPIED_INDEX])
|
||||
: parseCopiesFileList(attributes[FILES_COPIED_INDEX]);
|
||||
// Only keep renames, i.e. copies where the source file is also deleted.
|
||||
Iterator<String> keys = copies.keySet().iterator();
|
||||
while (keys.hasNext()) {
|
||||
String s = keys.next();
|
||||
if (filesAdded.contains(copies.get(s)) && filesDeleted.contains(s)) {
|
||||
filesAdded.remove(copies.get(s));
|
||||
filesDeleted.remove(s);
|
||||
}
|
||||
else if (!filesDeleted.contains(s)) {
|
||||
keys.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revisions.add(
|
||||
new HgFileRevision(myProject, hgFile, vcsRevisionNumber, branchName, revisionDate, author, commitMessage,
|
||||
filesModified,
|
||||
filesAdded,
|
||||
filesDeleted, copies));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
LOG.warn("Error parsing rev in line " + line);
|
||||
}
|
||||
catch (ParseException e) {
|
||||
LOG.warn("Error parsing date in line " + line);
|
||||
}
|
||||
}
|
||||
return revisions;
|
||||
return HgHistoryUtil.getCommitRecords(myProject, result,
|
||||
new HgFileRevisionLogParser(myProject, originalHgFile, myVersion));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -242,125 +135,4 @@ public class HgLogCommand {
|
||||
return commandExecutor.executeInCurrentThread(repo, "log", arguments);
|
||||
}
|
||||
|
||||
private static Set<String> parseFileList(String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
else {
|
||||
return new HashSet<String>(Arrays.asList(fileListString.split(" ")));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Map<String, String> parseCopiesFileList(@Nullable String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, String> copies = new HashMap<String, String>();
|
||||
String[] filesList = fileListString.split(HgChangesetUtil.FILE_SEPARATOR);
|
||||
|
||||
for (String pairOfFiles : filesList) {
|
||||
String[] files = pairOfFiles.split("\\s+\\(");
|
||||
if (files.length != 2) {
|
||||
LOG.info("Couldn't parse copied files: " + fileListString);
|
||||
return copies;
|
||||
}
|
||||
copies.put(files[1].substring(0, files[1].length() - 1), files[0]);
|
||||
}
|
||||
return copies;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Map<String, String> parseCopiesFileListAsOldVersion(@Nullable String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
else {
|
||||
Map<String, String> copies = new HashMap<String, String>();
|
||||
//hg copied files output looks like: "target1 (source1)target2 (source2)target3 .... (target_n)"
|
||||
//so we should split i-1 source from i target.
|
||||
// If some sources or targets contains '(' we suppose that it has Regular Bracket sequence and perform appropriate string parsing.
|
||||
//if it fails just return. (to avoid ArrayIndexOutOfBoundsException)
|
||||
String[] filesList = fileListString.split("\\s+\\(");
|
||||
String target = filesList[0];
|
||||
|
||||
for (int i = 1; i < filesList.length; ++i) {
|
||||
String source = filesList[i];
|
||||
int afterRightBraceIndex = findRightBracePosition(source);
|
||||
if (afterRightBraceIndex == -1) {
|
||||
break;
|
||||
}
|
||||
copies.put(source.substring(0, afterRightBraceIndex - 1), target);
|
||||
if (afterRightBraceIndex >= source.length()) { //the last 'word' in str
|
||||
break;
|
||||
}
|
||||
target = source.substring(afterRightBraceIndex);
|
||||
}
|
||||
return copies;
|
||||
}
|
||||
}
|
||||
|
||||
private static int findRightBracePosition(@NotNull String str) {
|
||||
int len = str.length();
|
||||
int depth = 1;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
char c = str.charAt(i);
|
||||
switch (c) {
|
||||
case '(':
|
||||
depth++;
|
||||
break;
|
||||
case ')':
|
||||
depth--;
|
||||
break;
|
||||
}
|
||||
if (depth == 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
LOG.info("Unexpected output during parse copied files in log command " + str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String[] constructTemplateArgument(boolean includeFiles, @NotNull HgVersion currentVersion) {
|
||||
List<String> templates = new ArrayList<String>();
|
||||
templates.add("{rev}");
|
||||
templates.add("{node}");
|
||||
if (currentVersion.isParentRevisionTemplateSupported()) {
|
||||
templates.add("{p1rev}:{p1node} {p2rev}:{p2node}");
|
||||
}
|
||||
else {
|
||||
templates.add("{parents}");
|
||||
}
|
||||
templates.addAll(Arrays.asList("{date|isodatesec}", "{author}", "{branch}", "{desc}"));
|
||||
if (!includeFiles) {
|
||||
return ArrayUtil.toStringArray(templates);
|
||||
}
|
||||
templates.addAll(Arrays.asList("{file_adds}", "{file_mods}", "{file_dels}"));
|
||||
templates
|
||||
.add(currentVersion.isBuiltInFunctionSupported() ? "{join(file_copies,'" + HgChangesetUtil.FILE_SEPARATOR + "')}" : "{file_copies}");
|
||||
return ArrayUtil.toStringArray(templates);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<HgRevisionNumber> parseParentRevisions(@NotNull String parentsString, @NotNull String currentRevisionString) {
|
||||
List<HgRevisionNumber> parents = new ArrayList<HgRevisionNumber>(2);
|
||||
if (StringUtil.isEmptyOrSpaces(parentsString)) {
|
||||
// parents shouldn't be empty only if not supported
|
||||
Long revision = Long.valueOf(currentRevisionString);
|
||||
HgRevisionNumber parentRevision = HgRevisionNumber.getLocalInstance(String.valueOf(revision - 1));
|
||||
parents.add(parentRevision);
|
||||
return parents;
|
||||
}
|
||||
//hg returns parents in the format 'rev:node rev:node ' (note the trailing space)
|
||||
String[] parentStrings = parentsString.trim().split(" ");
|
||||
for (String parentString : parentStrings) {
|
||||
String[] parentParts = parentString.split(":");
|
||||
//if revision has only 1 parent and "--debug" argument were added or if appropriate parent template were used, its second parent has revision number -1
|
||||
if (Integer.valueOf(parentParts[0]) >= 0) {
|
||||
parents.add(HgRevisionNumber.getInstance(parentParts[0], parentParts[1]));
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.zmlx.hg4idea.log;
|
||||
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.SmartList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.zmlx.hg4idea.HgRevisionNumber;
|
||||
import org.zmlx.hg4idea.util.HgChangesetUtil;
|
||||
import org.zmlx.hg4idea.util.HgUtil;
|
||||
import org.zmlx.hg4idea.util.HgVersion;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Parse one log command line and create appropriate type of commit info or revision info.
|
||||
* see {@link HgHistoryUtil}
|
||||
*
|
||||
* @param <CommitT>
|
||||
*/
|
||||
public abstract class HgBaseLogParser<CommitT> implements Function<String, CommitT> {
|
||||
private static final Logger LOG = Logger.getInstance(HgBaseLogParser.class);
|
||||
|
||||
private static final int REVISION_INDEX = 0;
|
||||
private static final int CHANGESET_INDEX = 1;
|
||||
private static final int PARENTS_INDEX = 2;
|
||||
private static final int DATE_INDEX = 3;
|
||||
private static final int AUTHOR_INDEX = 4;
|
||||
|
||||
protected static final int MESSAGE_INDEX = 5;
|
||||
protected static final int BRANCH_INDEX = 6;
|
||||
|
||||
protected static final int FILES_ADDED_INDEX = 7;
|
||||
protected static final int FILES_MODIFIED_INDEX = 8;
|
||||
protected static final int FILES_DELETED_INDEX = 9;
|
||||
protected static final int FILES_COPIED_INDEX = 10;
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
||||
|
||||
|
||||
@Nullable
|
||||
public CommitT convert(@NotNull String line) {
|
||||
|
||||
// we need to get all attributes, include empty trailing strings, so use non-positive limit as second argument
|
||||
List<String> attributes = StringUtil.split(line, HgChangesetUtil.ITEM_SEPARATOR, true, false);
|
||||
int numAttributes = attributes.size();
|
||||
if (numAttributes <= AUTHOR_INDEX) {
|
||||
LOG.info("Hg Log Command was cancelled or failed");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String revisionString = attributes.get(REVISION_INDEX);
|
||||
String changeset = attributes.get(CHANGESET_INDEX);
|
||||
String parentsString = attributes.get(PARENTS_INDEX);
|
||||
|
||||
SmartList<HgRevisionNumber> parents = parseParentRevisions(parentsString, revisionString);
|
||||
|
||||
Date revisionDate = DATE_FORMAT.parse(attributes.get(DATE_INDEX));
|
||||
Pair<String, String> authorAndEmail = HgUtil.parseUserNameAndEmail(attributes.get(AUTHOR_INDEX));
|
||||
return convertDetails(revisionString, changeset, parents, revisionDate, authorAndEmail.first, authorAndEmail.second, attributes);
|
||||
}
|
||||
catch (ParseException e) {
|
||||
LOG.warn("Error parsing date in line " + line);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommitT fun(String s) {
|
||||
return convert(s);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract CommitT convertDetails(@NotNull String rev,
|
||||
@NotNull String changeset,
|
||||
@NotNull SmartList<HgRevisionNumber> parents,
|
||||
@NotNull Date revisionDate,
|
||||
@NotNull String author,
|
||||
@NotNull String email,
|
||||
@NotNull List<String> attributes);
|
||||
|
||||
@NotNull
|
||||
public static List<String> constructDefaultTemplate(HgVersion currentVersion) {
|
||||
List<String> templates = new ArrayList<String>();
|
||||
templates.add("{rev}");
|
||||
templates.add("{node}");
|
||||
if (currentVersion.isParentRevisionTemplateSupported()) {
|
||||
templates.add("{p1rev}:{p1node} {p2rev}:{p2node}");
|
||||
}
|
||||
else {
|
||||
templates.add("{parents}");
|
||||
}
|
||||
templates.addAll(Arrays.asList("{date|isodatesec}", "{author}"));
|
||||
return templates;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String[] constructFullTemplateArgument(boolean includeFiles, @NotNull HgVersion currentVersion) {
|
||||
List<String> templates = new ArrayList<String>();
|
||||
templates.add("{rev}");
|
||||
templates.add("{node}");
|
||||
if (currentVersion.isParentRevisionTemplateSupported()) {
|
||||
templates.add("{p1rev}:{p1node} {p2rev}:{p2node}");
|
||||
}
|
||||
else {
|
||||
templates.add("{parents}");
|
||||
}
|
||||
templates.addAll(Arrays.asList("{date|isodatesec}", "{author}", "{desc}", "{branch}"));
|
||||
if (!includeFiles) {
|
||||
return ArrayUtil.toStringArray(templates);
|
||||
}
|
||||
templates.addAll(Arrays.asList("{file_adds}", "{file_mods}", "{file_dels}"));
|
||||
templates
|
||||
.add(currentVersion.isBuiltInFunctionSupported() ? "{join(file_copies,'" + HgChangesetUtil.FILE_SEPARATOR + "')}" : "{file_copies}");
|
||||
return ArrayUtil.toStringArray(templates);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static SmartList<HgRevisionNumber> parseParentRevisions(@NotNull String parentsString, @NotNull String currentRevisionString) {
|
||||
SmartList<HgRevisionNumber> parents = new SmartList<HgRevisionNumber>();
|
||||
if (StringUtil.isEmptyOrSpaces(parentsString)) {
|
||||
// parents shouldn't be empty only if not supported
|
||||
Long revision = Long.valueOf(currentRevisionString);
|
||||
HgRevisionNumber parentRevision = HgRevisionNumber.getLocalInstance(String.valueOf(revision - 1));
|
||||
parents.add(parentRevision);
|
||||
return parents;
|
||||
}
|
||||
//hg returns parents in the format 'rev:node rev:node ' (note the trailing space)
|
||||
List<String> parentStrings = StringUtil.split(parentsString.trim(), " ");
|
||||
for (String parentString : parentStrings) {
|
||||
List<String> parentParts = StringUtil.split(parentString, ":");
|
||||
//if revision has only 1 parent and "--debug" argument were added or if appropriate parent template were used,
|
||||
// its second parent has revision number -1
|
||||
if (Integer.valueOf(parentParts.get(0)) >= 0) {
|
||||
parents.add(HgRevisionNumber.getInstance(parentParts.get(0), parentParts.get(1)));
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static String parseAdditionalStringAttribute(final List<String> attributes, int index) {
|
||||
int numAttributes = attributes.size();
|
||||
if (numAttributes > index) {
|
||||
return attributes.get(index);
|
||||
}
|
||||
LOG.warn("Couldn't parse hg log commit info attribute " + index);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.zmlx.hg4idea.log;
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.util.SmartList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.zmlx.hg4idea.HgFile;
|
||||
import org.zmlx.hg4idea.HgFileRevision;
|
||||
import org.zmlx.hg4idea.HgRevisionNumber;
|
||||
import org.zmlx.hg4idea.util.HgChangesetUtil;
|
||||
import org.zmlx.hg4idea.util.HgVersion;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class HgFileRevisionLogParser extends HgBaseLogParser<HgFileRevision> {
|
||||
private static final Logger LOG = Logger.getInstance(HgFileRevisionLogParser.class);
|
||||
@NotNull private final HgFile myHgFile;
|
||||
@NotNull private final Project myProject;
|
||||
@NotNull private HgVersion myVersion;
|
||||
|
||||
public HgFileRevisionLogParser(@NotNull Project project, @NotNull HgFile hgFile, @NotNull HgVersion currentVersion) {
|
||||
myProject = project;
|
||||
myHgFile = hgFile;
|
||||
myVersion = currentVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected HgFileRevision convertDetails(@NotNull String rev,
|
||||
@NotNull String changeset,
|
||||
@NotNull SmartList<HgRevisionNumber> parents,
|
||||
@NotNull Date revisionDate,
|
||||
@NotNull String author,
|
||||
@NotNull String email,
|
||||
@NotNull List<String> attributes) {
|
||||
int numAttributes = attributes.size();
|
||||
String commitMessage = parseAdditionalStringAttribute(attributes, MESSAGE_INDEX);
|
||||
String branchName = parseAdditionalStringAttribute(attributes, BRANCH_INDEX);
|
||||
final HgRevisionNumber vcsRevisionNumber = new HgRevisionNumber(rev, changeset, author, commitMessage, parents);
|
||||
|
||||
Set<String> filesAdded = Collections.emptySet();
|
||||
Set<String> filesModified = Collections.emptySet();
|
||||
Set<String> filesDeleted = Collections.emptySet();
|
||||
Map<String, String> copies = Collections.emptyMap();
|
||||
boolean shouldParseOldTemplate = !myVersion.isBuiltInFunctionSupported();
|
||||
// At least in the case of the long template, it's OK that we don't have everything...for example, if there were no
|
||||
// deleted or copied files, then we won't get any attributes for them...
|
||||
if (numAttributes > FILES_ADDED_INDEX) {
|
||||
filesAdded = parseFileList(attributes.get(FILES_ADDED_INDEX));
|
||||
|
||||
if (numAttributes > FILES_MODIFIED_INDEX) {
|
||||
filesModified = parseFileList(attributes.get(FILES_MODIFIED_INDEX));
|
||||
|
||||
if (numAttributes > FILES_DELETED_INDEX) {
|
||||
filesDeleted = parseFileList(attributes.get(FILES_DELETED_INDEX));
|
||||
|
||||
if (numAttributes > FILES_COPIED_INDEX) {
|
||||
copies = shouldParseOldTemplate
|
||||
? parseCopiesFileListAsOldVersion(attributes.get(FILES_COPIED_INDEX))
|
||||
: parseCopiesFileList(attributes.get(FILES_COPIED_INDEX));
|
||||
// Only keep renames, i.e. copies where the source file is also deleted.
|
||||
Iterator<String> keys = copies.keySet().iterator();
|
||||
while (keys.hasNext()) {
|
||||
String s = keys.next();
|
||||
if (filesAdded.contains(copies.get(s)) && filesDeleted.contains(s)) {
|
||||
filesAdded.remove(copies.get(s));
|
||||
filesDeleted.remove(s);
|
||||
}
|
||||
else if (!filesDeleted.contains(s)) {
|
||||
keys.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new HgFileRevision(myProject, myHgFile, vcsRevisionNumber, branchName, revisionDate, author, commitMessage,
|
||||
filesModified, filesAdded, filesDeleted, copies);
|
||||
}
|
||||
|
||||
private static Set<String> parseFileList(@Nullable String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
else {
|
||||
return new HashSet<String>(StringUtil.split(fileListString," "));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static Map<String, String> parseCopiesFileList(@Nullable String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, String> copies = new HashMap<String, String>();
|
||||
List<String> filesList = StringUtil.split(fileListString, HgChangesetUtil.FILE_SEPARATOR);
|
||||
|
||||
for (String pairOfFiles : filesList) {
|
||||
String[] files = pairOfFiles.split("\\s+\\(");
|
||||
if (files.length != 2) {
|
||||
LOG.info("Couldn't parse copied files: " + fileListString);
|
||||
return copies;
|
||||
}
|
||||
copies.put(files[1].substring(0, files[1].length() - 1), files[0]);
|
||||
}
|
||||
return copies;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static Map<String, String> parseCopiesFileListAsOldVersion(@Nullable String fileListString) {
|
||||
if (StringUtil.isEmpty(fileListString)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
else {
|
||||
Map<String, String> copies = new HashMap<String, String>();
|
||||
//hg copied files output looks like: "target1 (source1)target2 (source2)target3 .... (target_n)"
|
||||
//so we should split i-1 source from i target.
|
||||
// If some sources or targets contains '(' we suppose that it has Regular Bracket sequence and perform appropriate string parsing.
|
||||
//if it fails just return. (to avoid ArrayIndexOutOfBoundsException)
|
||||
String[] filesList = fileListString.split("\\s+\\(");
|
||||
String target = filesList[0];
|
||||
|
||||
for (int i = 1; i < filesList.length; ++i) {
|
||||
String source = filesList[i];
|
||||
int afterRightBraceIndex = findRightBracePosition(source);
|
||||
if (afterRightBraceIndex == -1) {
|
||||
break;
|
||||
}
|
||||
copies.put(source.substring(0, afterRightBraceIndex - 1), target);
|
||||
if (afterRightBraceIndex >= source.length()) { //the last 'word' in str
|
||||
break;
|
||||
}
|
||||
target = source.substring(afterRightBraceIndex);
|
||||
}
|
||||
return copies;
|
||||
}
|
||||
}
|
||||
|
||||
private static int findRightBracePosition(@NotNull String str) {
|
||||
int len = str.length();
|
||||
int depth = 1;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
char c = str.charAt(i);
|
||||
switch (c) {
|
||||
case '(':
|
||||
depth++;
|
||||
break;
|
||||
case ')':
|
||||
depth--;
|
||||
break;
|
||||
}
|
||||
if (depth == 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
LOG.info("Unexpected output during parse copied files in log command " + str);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.zmlx.hg4idea.log;
|
||||
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.ThrowableComputable;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.FilePath;
|
||||
import com.intellij.openapi.vcs.FileStatus;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.VcsNotifier;
|
||||
import com.intellij.openapi.vcs.changes.Change;
|
||||
import com.intellij.openapi.vcs.changes.ChangeListManager;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.Consumer;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.SmartList;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.vcs.log.*;
|
||||
import com.intellij.vcsUtil.VcsUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.zmlx.hg4idea.*;
|
||||
import org.zmlx.hg4idea.command.HgLogCommand;
|
||||
import org.zmlx.hg4idea.execution.HgCommandResult;
|
||||
import org.zmlx.hg4idea.util.HgChangesetUtil;
|
||||
import org.zmlx.hg4idea.util.HgUtil;
|
||||
import org.zmlx.hg4idea.util.HgVersion;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class HgHistoryUtil {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(HgHistoryUtil.class);
|
||||
|
||||
private HgHistoryUtil() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<? extends VcsCommitMetadata> loadMetadata(@NotNull final Project project,
|
||||
@NotNull final VirtualFile root, int limit,
|
||||
@NotNull List<String> parameters) throws VcsException {
|
||||
|
||||
final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
|
||||
if (factory == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HgVcs hgvcs = HgVcs.getInstance(project);
|
||||
assert hgvcs != null;
|
||||
HgVersion version = hgvcs.getVersion();
|
||||
List<String> templateList = HgBaseLogParser.constructDefaultTemplate(version);
|
||||
templateList.add("{desc}");
|
||||
String[] templates = ArrayUtil.toStringArray(templateList);
|
||||
HgCommandResult result = getLogResult(project, root, version, limit, parameters, HgChangesetUtil.makeTemplate(templates));
|
||||
HgBaseLogParser<VcsCommitMetadata> baseParser = new HgBaseLogParser<VcsCommitMetadata>() {
|
||||
|
||||
@Override
|
||||
protected VcsCommitMetadata convertDetails(@NotNull String rev,
|
||||
@NotNull String changeset,
|
||||
@NotNull SmartList<HgRevisionNumber> parents,
|
||||
@NotNull Date revisionDate,
|
||||
@NotNull String author,
|
||||
@NotNull String email,
|
||||
@NotNull List<String> attributes) {
|
||||
String message = parseAdditionalStringAttribute(attributes, MESSAGE_INDEX);
|
||||
int subjectIndex = message.indexOf('\n');
|
||||
String subject = subjectIndex == -1 ? message : message.substring(0, subjectIndex);
|
||||
List<Hash> parentsHash = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : parents) {
|
||||
parentsHash.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
return factory.createCommitMetadata(factory.createHash(changeset), parentsHash, revisionDate.getTime(), root,
|
||||
subject, author, email, message, author, email, revisionDate.getTime());
|
||||
}
|
||||
};
|
||||
return getCommitRecords(project, result, baseParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Get & parse hg log detailed output with commits, their parents and their changes.</p>
|
||||
* <p/>
|
||||
* <p>Warning: this is method is efficient by speed, but don't query too much, because the whole log output is retrieved at once,
|
||||
* and it can occupy too much memory. The estimate is ~600Kb for 1000 commits.</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static List<? extends VcsFullCommitDetails> history(@NotNull final Project project, @NotNull final VirtualFile root, int limit,
|
||||
@NotNull List<String> parameters) throws VcsException {
|
||||
final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
|
||||
if (factory == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HgVcs hgvcs = HgVcs.getInstance(project);
|
||||
assert hgvcs != null;
|
||||
final HgVersion version = hgvcs.getVersion();
|
||||
String[] templates = HgBaseLogParser.constructFullTemplateArgument(true, version);
|
||||
HgCommandResult result = getLogResult(project, root, version, limit, parameters, HgChangesetUtil.makeTemplate(templates));
|
||||
List<HgFileRevision> hgRevisions =
|
||||
getCommitRecords(project, result, new HgFileRevisionLogParser(project, getOriginalHgFile(project, root), version));
|
||||
List<VcsFullCommitDetails> vcsFullCommitDetailsList = new ArrayList<VcsFullCommitDetails>();
|
||||
for (HgFileRevision revision : hgRevisions) {
|
||||
|
||||
HgRevisionNumber vcsRevisionNumber = revision.getRevisionNumber();
|
||||
List<HgRevisionNumber> parents = vcsRevisionNumber.getParents();
|
||||
HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root
|
||||
List<Hash> parentsHash = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : parents) {
|
||||
parentsHash.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
|
||||
final Collection<Change> changes = new ArrayList<Change>();
|
||||
for (String file : revision.getModifiedFiles()) {
|
||||
changes.add(createChange(project, root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
|
||||
}
|
||||
for (String file : revision.getAddedFiles()) {
|
||||
changes.add(createChange(project, root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
|
||||
}
|
||||
for (String file : revision.getDeletedFiles()) {
|
||||
changes.add(createChange(project, root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
|
||||
}
|
||||
for (Map.Entry<String, String> copiedFile : revision.getCopiedFiles().entrySet()) {
|
||||
changes
|
||||
.add(createChange(project, root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber, FileStatus.ADDED));
|
||||
}
|
||||
|
||||
vcsFullCommitDetailsList.add(factory.createFullDetails(factory.createHash(vcsRevisionNumber.getChangeset()), parentsHash,
|
||||
revision.getRevisionDate().getTime(), root,
|
||||
vcsRevisionNumber.getSubject(),
|
||||
vcsRevisionNumber.getAuthor(), vcsRevisionNumber.getEmail(),
|
||||
vcsRevisionNumber.getCommitMessage(), vcsRevisionNumber.getAuthor(),
|
||||
vcsRevisionNumber.getEmail(), revision.getRevisionDate().getTime(),
|
||||
new ThrowableComputable<Collection<Change>, Exception>() {
|
||||
@Override
|
||||
public Collection<Change> compute() throws Exception {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
return vcsFullCommitDetailsList;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private static HgCommandResult getLogResult(@NotNull final Project project,
|
||||
@NotNull final VirtualFile root, @NotNull HgVersion version, int limit,
|
||||
@NotNull List<String> parameters, @NotNull String template) {
|
||||
HgFile originalHgFile = getOriginalHgFile(project, root);
|
||||
HgLogCommand hgLogCommand = new HgLogCommand(project);
|
||||
List<String> args = new ArrayList<String>(parameters);
|
||||
hgLogCommand.setLogFile(false);
|
||||
if (!version.isParentRevisionTemplateSupported()) {
|
||||
args.add("--debug");
|
||||
}
|
||||
return hgLogCommand.execute(root, template, limit, originalHgFile, args);
|
||||
}
|
||||
|
||||
public static HgFile getOriginalHgFile(Project project, VirtualFile root) {
|
||||
HgFile hgFile = new HgFile(root, VcsUtil.getFilePath(root.getPath()));
|
||||
if (project.isDisposed()) {
|
||||
return hgFile;
|
||||
}
|
||||
FilePath originalFileName = HgUtil.getOriginalFileName(hgFile.toFilePath(), ChangeListManager.getInstance(project));
|
||||
return new HgFile(hgFile.getRepo(), originalFileName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static <CommitInfo> List<CommitInfo> getCommitRecords(@NotNull Project project,
|
||||
@Nullable HgCommandResult result,
|
||||
@NotNull Function<String, CommitInfo> converter) {
|
||||
final List<CommitInfo> revisions = new LinkedList<CommitInfo>();
|
||||
if (result == null) {
|
||||
return revisions;
|
||||
}
|
||||
|
||||
List<String> errors = result.getErrorLines();
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
if (result.getExitValue() != 0) {
|
||||
VcsNotifier.getInstance(project).notifyError(HgVcsMessages.message("hg4idea.error.log.command.execution"), errors.toString());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
LOG.warn(errors.toString());
|
||||
}
|
||||
String output = result.getRawOutput();
|
||||
List<String> changeSets = StringUtil.split(output, HgChangesetUtil.CHANGESET_SEPARATOR);
|
||||
return ContainerUtil.mapNotNull(changeSets, converter);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<? extends VcsShortCommitDetails> readMiniDetails(@NotNull Project project,
|
||||
@NotNull final VirtualFile root,
|
||||
@NotNull List<String> hashes)
|
||||
throws VcsException {
|
||||
final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
|
||||
if (factory == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HgVcs hgvcs = HgVcs.getInstance(project);
|
||||
assert hgvcs != null;
|
||||
HgVersion version = hgvcs.getVersion();
|
||||
List<String> templateList = HgBaseLogParser.constructDefaultTemplate(version);
|
||||
templateList.add("{desc}");
|
||||
String[] templates = ArrayUtil.toStringArray(templateList);
|
||||
HgCommandResult result = getLogResult(project, root, version, -1, prepareHashes(hashes), HgChangesetUtil.makeTemplate(templates));
|
||||
return getCommitRecords(project, result, new HgBaseLogParser<VcsShortCommitDetails>() {
|
||||
@Override
|
||||
protected VcsShortCommitDetails convertDetails(@NotNull String rev,
|
||||
@NotNull String changeset,
|
||||
@NotNull SmartList<HgRevisionNumber> parents,
|
||||
@NotNull Date revisionDate,
|
||||
@NotNull String author,
|
||||
@NotNull String email,
|
||||
@NotNull List<String> attributes) {
|
||||
String message = parseAdditionalStringAttribute(attributes, MESSAGE_INDEX);
|
||||
int subjectIndex = message.indexOf('\n');
|
||||
String subject = subjectIndex == -1 ? message : message.substring(0, subjectIndex);
|
||||
List<Hash> parentsHash = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : parents) {
|
||||
parentsHash.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
return factory.createShortDetails(factory.createHash(changeset), parentsHash,
|
||||
revisionDate.getTime(), root,
|
||||
subject, author, email);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<TimedVcsCommit> readAllHashes(@NotNull Project project, @NotNull VirtualFile root,
|
||||
@NotNull final Consumer<VcsUser> userRegistry, @NotNull List<String> params)
|
||||
throws VcsException {
|
||||
|
||||
final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
|
||||
if (factory == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HgVcs hgvcs = HgVcs.getInstance(project);
|
||||
assert hgvcs != null;
|
||||
HgVersion version = hgvcs.getVersion();
|
||||
String[] templates = ArrayUtil.toStringArray(HgBaseLogParser.constructDefaultTemplate(version));
|
||||
HgCommandResult result = getLogResult(project, root, version, -1, params, HgChangesetUtil.makeTemplate(templates));
|
||||
return getCommitRecords(project, result, new HgBaseLogParser<TimedVcsCommit>() {
|
||||
|
||||
@Override
|
||||
protected TimedVcsCommit convertDetails(@NotNull String rev,
|
||||
@NotNull String changeset,
|
||||
@NotNull SmartList<HgRevisionNumber> parents,
|
||||
@NotNull Date revisionDate,
|
||||
@NotNull String author,
|
||||
@NotNull String email,
|
||||
@NotNull List<String> attributes) {
|
||||
List<Hash> parentsHash = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : parents) {
|
||||
parentsHash.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
userRegistry.consume(factory.createUser(author, email));
|
||||
return factory.createTimedCommit(factory.createHash(changeset),
|
||||
parentsHash, revisionDate.getTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static VcsLogObjectsFactory getObjectsFactoryWithDisposeCheck(Project project) {
|
||||
if (!project.isDisposed()) {
|
||||
return ServiceManager.getService(project, VcsLogObjectsFactory.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Change createChange(@NotNull Project project, @NotNull VirtualFile root,
|
||||
@Nullable String fileBefore,
|
||||
@Nullable HgRevisionNumber revisionBefore,
|
||||
@Nullable String fileAfter,
|
||||
HgRevisionNumber revisionAfter,
|
||||
FileStatus aStatus) {
|
||||
|
||||
HgContentRevision beforeRevision =
|
||||
fileBefore == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
|
||||
HgContentRevision afterRevision =
|
||||
fileAfter == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
|
||||
return new Change(beforeRevision, afterRevision, aStatus);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> prepareHashes(@NotNull List<String> hashes) {
|
||||
List<String> hashArgs = new ArrayList<String>();
|
||||
for (String hash : hashes) {
|
||||
hashArgs.add("-r");
|
||||
hashArgs.add(hash);
|
||||
}
|
||||
return hashArgs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Collection<String> getDescendingHeadsOfBranches(@NotNull Project project, @NotNull VirtualFile root, @NotNull Hash hash)
|
||||
throws VcsException {
|
||||
//hg log -r "descendants(659db54c1b6865c97c4497fa867194bcd759ca76) and head()" --template "{branch}{bookmarks}"
|
||||
Set<String> branchHeads = new HashSet<String>();
|
||||
List<String> params = new ArrayList<String>();
|
||||
params.add("-r");
|
||||
params.add("descendants(" + hash.asString() + ") and head()");
|
||||
HgLogCommand hgLogCommand = new HgLogCommand(project);
|
||||
hgLogCommand.setLogFile(false);
|
||||
String template = HgChangesetUtil.makeTemplate("{branch}", "{bookmarks}");
|
||||
HgCommandResult logResult = hgLogCommand.execute(root, template, -1, null, params);
|
||||
if (logResult == null || logResult.getExitValue() != 0) {
|
||||
throw new VcsException("Couldn't get commit details: log command execution error.");
|
||||
}
|
||||
String output = logResult.getRawOutput();
|
||||
List<String> changeSets = StringUtil.split(output, HgChangesetUtil.CHANGESET_SEPARATOR);
|
||||
for (String line : changeSets) {
|
||||
List<String> attributes = StringUtil.split(line, HgChangesetUtil.ITEM_SEPARATOR);
|
||||
branchHeads.addAll(attributes);
|
||||
}
|
||||
return branchHeads;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import org.zmlx.hg4idea.HgVcs;
|
||||
import org.zmlx.hg4idea.repo.HgConfig;
|
||||
import org.zmlx.hg4idea.repo.HgRepository;
|
||||
import org.zmlx.hg4idea.repo.HgRepositoryManager;
|
||||
import org.zmlx.hg4idea.util.HgHistoryUtil;
|
||||
import org.zmlx.hg4idea.util.HgUtil;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -88,6 +87,9 @@ public class HgLogProvider implements VcsLogProvider {
|
||||
@Override
|
||||
public Collection<VcsRef> readAllRefs(@NotNull VirtualFile root) throws VcsException {
|
||||
myRepositoryManager.waitUntilInitialized();
|
||||
if (myProject.isDisposed()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HgRepository repository = myRepositoryManager.getRepositoryForRoot(root);
|
||||
if (repository == null) {
|
||||
LOG.error("Repository not found for root " + root);
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2013 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.zmlx.hg4idea.util;
|
||||
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.ThrowableComputable;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.FileStatus;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.changes.Change;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.Consumer;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.SmartList;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.vcs.log.*;
|
||||
import com.intellij.vcsUtil.VcsUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.zmlx.hg4idea.*;
|
||||
import org.zmlx.hg4idea.action.HgCommandResultNotifier;
|
||||
import org.zmlx.hg4idea.command.HgLogCommand;
|
||||
import org.zmlx.hg4idea.execution.HgCommandException;
|
||||
import org.zmlx.hg4idea.execution.HgCommandResult;
|
||||
import org.zmlx.hg4idea.provider.HgCommittedChangeList;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class HgHistoryUtil {
|
||||
|
||||
private HgHistoryUtil() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<? extends VcsCommitMetadata> loadMetadata(@NotNull final Project project,
|
||||
@NotNull final VirtualFile root, int limit,
|
||||
List<String> parameters) throws VcsException {
|
||||
|
||||
final VcsLogObjectsFactory factory = ServiceManager.getService(project, VcsLogObjectsFactory.class);
|
||||
List<HgCommittedChangeList> result = getCommittedChangeList(project, root, limit, false, parameters);
|
||||
return ContainerUtil.mapNotNull(result, new Function<HgCommittedChangeList, VcsCommitMetadata>() {
|
||||
@Override
|
||||
public VcsCommitMetadata fun(HgCommittedChangeList record) {
|
||||
HgRevisionNumber revNumber = (HgRevisionNumber)record.getRevisionNumber();
|
||||
return factory.createCommitMetadata(factory.createHash(revNumber.getChangeset()), getParentHashes(factory, revNumber),
|
||||
record.getCommitDate().getTime(), root,
|
||||
revNumber.getSubject(),
|
||||
revNumber.getAuthor(), revNumber.getEmail(), revNumber.getCommitMessage(),
|
||||
record.getCommitterName(),
|
||||
"", record.getCommitDate().getTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Get & parse hg log detailed output with commits, their parents and their changes.</p>
|
||||
* <p/>
|
||||
* <p>Warning: this is method is efficient by speed, but don't query too much, because the whole log output is retrieved at once,
|
||||
* and it can occupy too much memory. The estimate is ~600Kb for 1000 commits.</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static List<? extends VcsFullCommitDetails> history(@NotNull final Project project, @NotNull final VirtualFile root, int limit,
|
||||
List<String> parameters) throws VcsException {
|
||||
final VcsLogObjectsFactory factory = ServiceManager.getService(project, VcsLogObjectsFactory.class);
|
||||
List<HgCommittedChangeList> result = getCommittedChangeList(project, root, limit, true, parameters);
|
||||
return ContainerUtil.mapNotNull(result, new Function<HgCommittedChangeList, VcsFullCommitDetails>() {
|
||||
@Override
|
||||
public VcsFullCommitDetails fun(HgCommittedChangeList record) {
|
||||
return createCommit(root, record, factory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<HgCommittedChangeList> getCommittedChangeList(@NotNull final Project project,
|
||||
@NotNull final VirtualFile root, int limit, boolean withFiles,
|
||||
List<String> parameters) {
|
||||
HgFile hgFile = new HgFile(root, VcsUtil.getFilePath(root.getPath()));
|
||||
List<HgCommittedChangeList> result = new LinkedList<HgCommittedChangeList>();
|
||||
final List<HgFileRevision> localRevisions;
|
||||
HgLogCommand hgLogCommand = new HgLogCommand(project);
|
||||
List<String> args = new ArrayList<String>(parameters);
|
||||
hgLogCommand.setLogFile(false);
|
||||
HgVcs hgvcs = HgVcs.getInstance(project);
|
||||
assert hgvcs != null;
|
||||
try {
|
||||
if (!hgvcs.getVersion().isParentRevisionTemplateSupported()) {
|
||||
args.add("--debug");
|
||||
}
|
||||
localRevisions = hgLogCommand.execute(hgFile, limit, withFiles, args);
|
||||
}
|
||||
catch (HgCommandException e) {
|
||||
new HgCommandResultNotifier(project).notifyError(null, HgVcsMessages.message("hg4idea.error.log.command.execution"), e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Collections.reverse(localRevisions);
|
||||
for (HgFileRevision revision : localRevisions) {
|
||||
HgRevisionNumber vcsRevisionNumber = revision.getRevisionNumber();
|
||||
List<HgRevisionNumber> parents = vcsRevisionNumber.getParents();
|
||||
|
||||
HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root
|
||||
|
||||
List<Change> changes = new ArrayList<Change>();
|
||||
for (String file : revision.getModifiedFiles()) {
|
||||
changes.add(createChange(project, root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
|
||||
}
|
||||
for (String file : revision.getAddedFiles()) {
|
||||
changes.add(createChange(project, root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
|
||||
}
|
||||
for (String file : revision.getDeletedFiles()) {
|
||||
changes.add(createChange(project, root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
|
||||
}
|
||||
for (Map.Entry<String, String> copiedFile : revision.getCopiedFiles().entrySet()) {
|
||||
changes
|
||||
.add(createChange(project, root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber, FileStatus.ADDED));
|
||||
}
|
||||
|
||||
result.add(new HgCommittedChangeList(hgvcs, vcsRevisionNumber, revision.getBranchName(), revision.getCommitMessage(),
|
||||
revision.getAuthor(), revision.getRevisionDate(), changes));
|
||||
}
|
||||
Collections.reverse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<? extends VcsShortCommitDetails> readMiniDetails(Project project, final VirtualFile root, List<String> hashes)
|
||||
throws VcsException {
|
||||
final VcsLogObjectsFactory factory = ServiceManager.getService(project, VcsLogObjectsFactory.class);
|
||||
return ContainerUtil.map(getCommittedChangeList(project, root, -1, false, prepareHashes(hashes)),
|
||||
new Function<HgCommittedChangeList, VcsShortCommitDetails>() {
|
||||
@Override
|
||||
public VcsShortCommitDetails fun(HgCommittedChangeList record) {
|
||||
HgRevisionNumber revNumber = (HgRevisionNumber)record.getRevisionNumber();
|
||||
List<Hash> parents = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : revNumber.getParents()) {
|
||||
parents.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
return factory.createShortDetails(factory.createHash(revNumber.getChangeset()), parents,
|
||||
record.getCommitDate().getTime(), root,
|
||||
revNumber.getSubject(), revNumber.getAuthor(), revNumber.getEmail());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<TimedVcsCommit> readAllHashes(@NotNull Project project, @NotNull VirtualFile root,
|
||||
@NotNull final Consumer<VcsUser> userRegistry, List<String> params) throws VcsException {
|
||||
|
||||
final VcsLogObjectsFactory factory = ServiceManager.getService(project, VcsLogObjectsFactory.class);
|
||||
return ContainerUtil.map(getCommittedChangeList(project, root, -1, false, params), new Function<HgCommittedChangeList, TimedVcsCommit>() {
|
||||
@Override
|
||||
public TimedVcsCommit fun(HgCommittedChangeList record) {
|
||||
HgRevisionNumber revNumber = (HgRevisionNumber)record.getRevisionNumber();
|
||||
List<Hash> parents = new SmartList<Hash>();
|
||||
for (HgRevisionNumber parent : revNumber.getParents()) {
|
||||
parents.add(factory.createHash(parent.getChangeset()));
|
||||
}
|
||||
userRegistry.consume(factory.createUser(record.getRevision().getAuthor(), revNumber.getEmail()));
|
||||
return factory.createTimedCommit(factory.createHash(revNumber.getChangeset()),
|
||||
parents, record.getCommitDate().getTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Change createChange(Project project, VirtualFile root,
|
||||
@Nullable String fileBefore,
|
||||
@Nullable HgRevisionNumber revisionBefore,
|
||||
@Nullable String fileAfter,
|
||||
HgRevisionNumber revisionAfter,
|
||||
FileStatus aStatus) {
|
||||
|
||||
HgContentRevision beforeRevision =
|
||||
fileBefore == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
|
||||
HgContentRevision afterRevision =
|
||||
fileAfter == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
|
||||
return new Change(beforeRevision, afterRevision, aStatus);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static VcsFullCommitDetails createCommit(@NotNull VirtualFile root,
|
||||
@NotNull final HgCommittedChangeList record, @NotNull VcsLogObjectsFactory factory) {
|
||||
|
||||
HgRevisionNumber revNumber = (HgRevisionNumber)record.getRevisionNumber();
|
||||
final Collection<Change> changes = record.getChanges();
|
||||
return factory.createFullDetails(factory.createHash(revNumber.getChangeset()), getParentHashes(factory, revNumber),
|
||||
record.getCommitDate().getTime(), root,
|
||||
revNumber.getSubject(),
|
||||
revNumber.getAuthor(), revNumber.getEmail(), revNumber.getCommitMessage(), record.getCommitterName(),
|
||||
"", record.getCommitDate().getTime(), new ThrowableComputable<Collection<Change>, Exception>() {
|
||||
@Override
|
||||
public Collection<Change> compute() throws Exception {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<Hash> getParentHashes(@NotNull final VcsLogObjectsFactory factory, @NotNull HgRevisionNumber revNumber) {
|
||||
return ContainerUtil.map(revNumber.getParents(), new Function<HgRevisionNumber, Hash>() {
|
||||
@Override
|
||||
public Hash fun(HgRevisionNumber parent) {
|
||||
return factory.createHash(parent.getChangeset());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static List<String> prepareHashes(@NotNull List<String> hashes) {
|
||||
List<String> hashArgs = new ArrayList<String>();
|
||||
for (String hash : hashes) {
|
||||
hashArgs.add("-r");
|
||||
hashArgs.add(hash);
|
||||
}
|
||||
return hashArgs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Collection<String> getDescendingHeadsOfBranches(@NotNull Project project, @NotNull VirtualFile root, @NotNull Hash hash)
|
||||
throws VcsException {
|
||||
//hg log -r "descendants(659db54c1b6865c97c4497fa867194bcd759ca76) and head()" --template "{branch}{bookmarks}"
|
||||
Set<String> branchHeads = new HashSet<String>();
|
||||
List<String> params = new ArrayList<String>();
|
||||
params.add("-r");
|
||||
params.add("descendants(" + hash.asString() + ") and head()");
|
||||
HgLogCommand hgLogCommand = new HgLogCommand(project);
|
||||
hgLogCommand.setLogFile(false);
|
||||
String template = HgChangesetUtil.makeTemplate("{branch}", "{bookmarks}");
|
||||
HgCommandResult logResult = hgLogCommand.execute(root, template, -1, null, params);
|
||||
if (logResult == null || logResult.getExitValue() != 0) {
|
||||
throw new VcsException("Couldn't get commit details: log command execution error.");
|
||||
}
|
||||
String output = logResult.getRawOutput();
|
||||
List<String> changeSets = StringUtil.split(output, HgChangesetUtil.CHANGESET_SEPARATOR);
|
||||
for (String line : changeSets) {
|
||||
List<String> attributes = StringUtil.split(line, HgChangesetUtil.ITEM_SEPARATOR);
|
||||
branchHeads.addAll(attributes);
|
||||
}
|
||||
return branchHeads;
|
||||
}
|
||||
}
|
||||
@@ -22,19 +22,14 @@ import com.intellij.vcs.log.Hash;
|
||||
import com.intellij.vcs.log.impl.HashImpl;
|
||||
import hg4idea.test.HgPlatformTest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.zmlx.hg4idea.util.HgHistoryUtil;
|
||||
import org.zmlx.hg4idea.log.HgHistoryUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.intellij.openapi.vcs.Executor.cd;
|
||||
import static com.intellij.openapi.vcs.Executor.echo;
|
||||
import static com.intellij.openapi.vcs.Executor.touch;
|
||||
import static com.intellij.openapi.vcs.Executor.*;
|
||||
import static hg4idea.test.HgExecutor.hg;
|
||||
|
||||
/**
|
||||
* @author Nadya Zabrodina
|
||||
*/
|
||||
public class HgLogHistoryTest extends HgPlatformTest {
|
||||
|
||||
@Override
|
||||
|
||||
+4
-7
@@ -13,11 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package hg4idea.test.history;
|
||||
package org.zmlx.hg4idea.log;
|
||||
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import org.junit.Test;
|
||||
import org.zmlx.hg4idea.command.HgLogCommand;
|
||||
import org.zmlx.hg4idea.util.HgUtil;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -25,14 +24,12 @@ import java.util.Map;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Nadya Zabrodina
|
||||
*/
|
||||
public class HgLogParseTest {
|
||||
|
||||
@Test
|
||||
public void testParseFileCopiesWithWhitespaces() {
|
||||
Map<String, String> filesMap = HgLogCommand.parseCopiesFileList("/a/b c/d.txt (a/b a/d.txt)\u0001/a/b c/(d).txt (/a/b c/(f).txt)");
|
||||
Map<String, String> filesMap = HgFileRevisionLogParser.parseCopiesFileList(
|
||||
"/a/b c/d.txt (a/b a/d.txt)\u0001/a/b c/(d).txt (/a/b c/(f).txt)");
|
||||
assertTrue(filesMap.containsKey("a/b a/d.txt"));
|
||||
assertTrue(filesMap.containsKey("/a/b c/(f).txt"));
|
||||
assertTrue(filesMap.containsValue("/a/b c/d.txt"));
|
||||
@@ -41,7 +38,7 @@ public class HgLogParseTest {
|
||||
|
||||
@Test
|
||||
public void testParseFileCopiesOldVersion() {
|
||||
Map<String, String> filesMap = HgLogCommand.parseCopiesFileListAsOldVersion(
|
||||
Map<String, String> filesMap = HgFileRevisionLogParser.parseCopiesFileListAsOldVersion(
|
||||
"/a/b c/d.txt (a/b a/d.txt)/a/b c/(d).txt (/a/b c/(f).txt)");
|
||||
assertTrue(filesMap.containsKey("a/b a/d.txt"));
|
||||
assertTrue(filesMap.containsKey("/a/b c/(f).txt"));
|
||||
Reference in New Issue
Block a user