package hudson.plugins.tfs.commands;
import hudson.plugins.tfs.model.ChangeSet;
import hudson.plugins.tfs.util.DateParser;
import hudson.plugins.tfs.util.DateUtil;
import hudson.plugins.tfs.util.KeyValueTextReader;
import hudson.plugins.tfs.util.MaskedArgumentListBuilder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DetailedHistoryCommand extends AbstractCommand implements ParseableCommand<List<ChangeSet>> {
// Setting this system property will skip the date chek in parsing that makes
// sure that a change set is within the date range. See CC-735 reference.
public static final String IGNORE_DATE_CHECK_ON_CHANGE_SET = "tfs.history.skipdatecheck";
private static final String CHANGESET_SEPERATOR = "------------";
/**
* The magic regex to identify the key data elements within the
* changeset
*/
private static final Pattern PATTERN_CHANGESET = Pattern.compile("^[^:]*:[ \t]([0-9]*)\n"
+ "[^:]*:[ \t](.*)\n[^:]*:[ \t](.*)\n"
+ "[^:]*:(?s)(.*)\n\n[^\n :]*:(?=\n )(.*)\n\n");
/**
* An additional regex to split the items into their parts (change type
* and filename)
*/
private static final Pattern PATTERN_ITEM = Pattern.compile("\\s*([^$]+) (\\$/.*)");
private final String projectPath;
private final Calendar fromTimestamp;
private final Calendar toTimestamp;
private final DateParser dateParser;
private final boolean skipDateCheckInParsing;
public DetailedHistoryCommand(ServerConfigurationProvider configurationProvider, String projectPath, Calendar fromTimestamp, Calendar toTimestamp,
DateParser dateParser) {
super(configurationProvider);
this.projectPath = projectPath;
this.fromTimestamp = fromTimestamp;
this.toTimestamp = toTimestamp;
this.dateParser = dateParser;
this.skipDateCheckInParsing = Boolean.valueOf(System.getProperty(IGNORE_DATE_CHECK_ON_CHANGE_SET));
}
public DetailedHistoryCommand(ServerConfigurationProvider provider,
String projectPath, Calendar fromTimestamp, Calendar toTimestamp) {
this(provider, projectPath, fromTimestamp, toTimestamp, new DateParser());
}
public MaskedArgumentListBuilder getArguments() {
MaskedArgumentListBuilder arguments = new MaskedArgumentListBuilder();
arguments.add("history");
arguments.add(projectPath);
arguments.add("-noprompt");
arguments.add(String.format("-version:D%s~D%s",
DateUtil.TFS_DATETIME_FORMATTER.get().format(fromTimestamp.getTime()),
DateUtil.TFS_DATETIME_FORMATTER.get().format(toTimestamp.getTime())));
arguments.add("-recursive");
arguments.add("-format:detailed");
addServerArgument(arguments);
addLoginArgument(arguments);
return arguments;
}
public List<ChangeSet> parse(Reader reader) throws IOException, ParseException {
Date lastBuildDate = fromTimestamp.getTime();
ArrayList<ChangeSet> list = new ArrayList<ChangeSet>();
ChangeSetStringReader iterator = new ChangeSetStringReader(new BufferedReader(reader));
String changeSetString = iterator.readChangeSet();
while (changeSetString != null) {
ChangeSet changeSet = parseChangeSetString(changeSetString);
// If some tf tool outputs the key words in non english we will use the old fashion way
// using the complicated regex
if (changeSet == null) {
changeSet = parseChangeSetStringWithRegex(changeSetString);
}
if (changeSet == null) {
// We should always find at least one item. If we don't
// then this will be because we have not parsed correctly.
throw new ParseException("Parse error. Unable to find an item within "
+ "a changeset. Please report this as a bug. Changeset"
+ "data = \"\n" + changeSetString + "\n\".",
0);
}
// CC-735. Ignore changesets that occured before the specified lastBuild.
if (skipDateCheckInParsing || changeSet.getDate().compareTo(lastBuildDate) > 0) {
list.add(changeSet);
}
changeSetString = iterator.readChangeSet();
}
Collections.reverse(list);
return list;
}
/**
* Returns a change set from the string containing one change set.
* This will do some intelligent parsing as it will read all key and value from the log.
* This will only work if we know the exact words in the key column, and as of now we only
* know of english. If it can not find the keys it will return null.
* @param changeSetString string containing one change set
* @return a change set if it could read the different key/value pairs; null otherwise
*/
private ChangeSet parseChangeSetString(String changeSetString) throws ParseException, IOException {
KeyValueTextReader reader = new KeyValueTextReader();
Map<String, String> map = reader.parse(changeSetString);
if (map.containsKey("User") && map.containsKey("Changeset") && map.containsKey("Date") && map.containsKey("Items")) {
ChangeSet changeSet = createChangeSet(map.get("Items"), map.get("Changeset"), map.get("User"), map.get("Date"), map.get("Comment"));
if (changeSet != null) {
changeSet.setCheckedInBy(map.get("Checked in by"));
}
return changeSet;
}
return null;
}
/**
* Returns a change set from the string containing ONE change set using a regex
* @param changeSetString string containing ONE change set output
* @return a change set; null if the change set was too old or invalid.
*/
private ChangeSet parseChangeSetStringWithRegex(String changeSetString) throws ParseException {
Matcher m = PATTERN_CHANGESET.matcher(changeSetString);
if (m.find()) {
String revision = m.group(1);
String userName = m.group(2).trim();
// Remove the indentation from the comment
String comment = m.group(4).replaceAll("\n ", "\n");
if (comment.length() > 0) {
// remove leading "\n"
comment = comment.trim();
}
return createChangeSet(m.group(5), revision, userName, m.group(3), comment);
}
return null;
}
private ChangeSet createChangeSet(String items, String revision, String userName, String modifiedTime, String comment) throws ParseException {
// Parse the items.
Matcher itemMatcher = PATTERN_ITEM.matcher(items);
ChangeSet changeset = null;
while (itemMatcher.find()) {
if (changeset == null) {
changeset = new ChangeSet(revision, dateParser.parseDate(modifiedTime), userName, comment);
}
// In a similar way to Subversion, TFS will record additions
// of folders etc
// Therefore we have to report all modifictaion by the file
// and not split
// into file and folder as there is no easy way to
// distinguish
// $/path/filename
// from
// $/path/foldername
//
String path = itemMatcher.group(2);
String action = itemMatcher.group(1).trim();
if (!path.startsWith("$/")) {
// If this happens then we have a bug, output some data
// to make it easy to figure out what the problem was so
// that we can fix it.
throw new ParseException("Parse error. Mistakenly identified \"" + path
+ "\" as an item, but it does not appear to "
+ "be a valid TFS path. Please report this as a bug. Changeset" + "data = \"\n"
+ items + "\n\".", itemMatcher.start());
}
changeset.getItems().add(new ChangeSet.Item(path, action));
}
return changeset;
}
/**
* Class for extracing one change set segment out of a long list of change sets.
*/
private static class ChangeSetStringReader {
private final BufferedReader reader;
private boolean foundAtLeastOneChangeSet;
public ChangeSetStringReader(BufferedReader reader) {
super();
this.reader = reader;
}
public String readChangeSet() throws IOException {
StringBuilder builder = new StringBuilder();
String line;
int linecount = 0;
while ((line = reader.readLine()) != null) {
if (line.startsWith(CHANGESET_SEPERATOR)) {
foundAtLeastOneChangeSet = true;
if (linecount > 1) {
// We are starting a new changeset.
return builder.toString();
}
} else {
linecount++;
builder.append(line).append('\n');
}
}
if (foundAtLeastOneChangeSet && linecount > 0) {
return builder.toString();
}
return null;
}
}
}