Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #358 from MarkEWaite/master-JENKINS-30073-timestam…
…p-parsing

[Fix JENKINS-30073] API timestamp return correct value
  • Loading branch information
MarkEWaite committed Oct 17, 2015
2 parents 4979ace + d5af3b9 commit 1eda492
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 4 deletions.
77 changes: 75 additions & 2 deletions src/main/java/hudson/plugins/git/GitChangeSet.java
Expand Up @@ -9,7 +9,6 @@
import hudson.scm.ChangeLogSet.AffectedFile;
import hudson.scm.EditType;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

Expand All @@ -23,11 +22,19 @@
import java.util.HashSet;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static hudson.Util.fixEmpty;

import org.joda.time.DateTime;
import org.joda.time.DateTimeFieldType;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.ISODateTimeFormat;

/**
* Represents a change set.
* @author Nigel Magnay
Expand All @@ -49,6 +56,10 @@ public class GitChangeSet extends ChangeLogSet.Entry {
private static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss";
private static final String ISO_8601_WITH_TZ = "yyyy-MM-dd'T'HH:mm:ssX";

private final DateTimeFormatter [] dateFormatters;

public static final Logger LOGGER = Logger.getLogger(GitChangeSet.class.getName());

/**
* This is broken as a part of the 1.5 refactoring.
*
Expand Down Expand Up @@ -92,6 +103,51 @@ public GitChangeSet(List<String> lines, boolean authorOrCommitter) {
if (lines.size() > 0) {
parseCommit(lines);
}

// Nearly ISO dates generated by git whatchanged --format=+ci
// Look like '2015-09-30 08:21:24 -0600'
// ISO is '2015-09-30T08:21:24-06:00'
// Uses Builder rather than format pattern for more reliable parsing
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendFixedDecimal(DateTimeFieldType.year(), 4);
builder.appendLiteral('-');
builder.appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2);
builder.appendLiteral('-');
builder.appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2);
builder.appendLiteral(' ');
builder.appendFixedDecimal(DateTimeFieldType.hourOfDay(), 2);
builder.appendLiteral(':');
builder.appendFixedDecimal(DateTimeFieldType.minuteOfHour(), 2);
builder.appendLiteral(':');
builder.appendFixedDecimal(DateTimeFieldType.secondOfMinute(), 2);
builder.appendLiteral(' ');
builder.appendTimeZoneOffset(null, false, 2, 2);
DateTimeFormatter gitDateFormatter = builder.toFormatter();

// DateTimeFormat.forPattern("yyyy-MM-DDTHH:mm:ssZ");
// 2013-03-21T15:16:44+0100
// Uses Builder rather than format pattern for more reliable parsing
builder = new DateTimeFormatterBuilder();
builder.appendFixedDecimal(DateTimeFieldType.year(), 4);
builder.appendLiteral('-');
builder.appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2);
builder.appendLiteral('-');
builder.appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2);
builder.appendLiteral('T');
builder.appendFixedDecimal(DateTimeFieldType.hourOfDay(), 2);
builder.appendLiteral(':');
builder.appendFixedDecimal(DateTimeFieldType.minuteOfHour(), 2);
builder.appendLiteral(':');
builder.appendFixedDecimal(DateTimeFieldType.secondOfMinute(), 2);
builder.appendTimeZoneOffset(null, false, 2, 2);
DateTimeFormatter nearlyISOFormatter = builder.toFormatter();

DateTimeFormatter isoDateFormat = ISODateTimeFormat.basicDateTimeNoMillis();

dateFormatters = new DateTimeFormatter[3];
dateFormatters[0] = gitDateFormatter; // First priority +%cI format
dateFormatters[1] = nearlyISOFormatter; // Second priority seen in git-plugin
dateFormatters[2] = isoDateFormat; // Third priority, ISO 8601 format
}

private void parseCommit(List<String> lines) {
Expand Down Expand Up @@ -210,8 +266,25 @@ public String getDate() {

@Override
public long getTimestamp() {
String date = getDate();
if (date == null) {
LOGGER.log(Level.WARNING, "Failed to parse null date");
return -1;
}
if (date.isEmpty()) {
LOGGER.log(Level.WARNING, "Failed to parse empty date");
return -1;
}

for (DateTimeFormatter dateFormatter : dateFormatters) {
try {
DateTime dateTime = DateTime.parse(date, dateFormatter);
return dateTime.getMillis();
} catch (IllegalArgumentException ia) {
}
}
try {
return new SimpleDateFormat(ISO_8601_WITH_TZ).parse(getDate()).getTime();
return new SimpleDateFormat(ISO_8601_WITH_TZ).parse(date).getTime();
} catch (ParseException e) {
return -1;
} catch (IllegalArgumentException ia) {
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetBasicTest.java
Expand Up @@ -42,6 +42,24 @@ public void testAuthor() {
assertEquals(GitChangeSetUtil.AUTHOR_NAME, genChangeSet(true, false).getAuthorName());
}

@Test
public void testGetDate() {
assertEquals("1970-01-15T06:56:08-0600", genChangeSet(true, false).getDate());
}

@Test
public void testGetTimestamp() {
assertEquals(1256168000L, genChangeSet(true, false).getTimestamp());
}

@Test
public void testInvalidDate() {
final String badDateString = "2015-03-03x09:22:42 -0700";
GitChangeSet c = new GitChangeSet(Arrays.asList("author John Doe <john.doe@jenkins-ci.org> " + badDateString), true);
assertEquals(badDateString, c.getDate());
assertEquals(-1L, c.getTimestamp());
}

@Test
public void testIsoDate() {

Expand Down Expand Up @@ -100,4 +118,14 @@ public void testSwedishCommitterName() {
public void testSwedishAuthorName() {
assertEquals("misterÅ", genChangeSetForSwedCase(true).getAuthorName());
}

@Test
public void testSwedishDate() {
assertEquals("2013-03-21T15:16:44+0100", genChangeSetForSwedCase(true).getDate());
}

@Test
public void testSwedishTimestamp() {
assertEquals(1363875404000L, genChangeSetForSwedCase(true).getTimestamp());
}
}
5 changes: 5 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetEmptyTest.java
Expand Up @@ -22,6 +22,11 @@ public void testGetDate() {
assertNull(changeSet.getDate());
}

@Test
public void testGetTimestamp() {
assertEquals(-1L, changeSet.getTimestamp());
}

@Test
public void testGetCommitId() {
assertNull(changeSet.getCommitId());
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetEuroTest.java
Expand Up @@ -124,6 +124,16 @@ public void testGetBranch() {
assertNull(changeSet.getBranch());
}

@Test
public void testGetDate() {
assertEquals(useAuthorName ? "2013-03-21T15:16:44+0100" : "2013-03-25T08:18:59-0400", changeSet.getDate());
}

@Test
public void testGetTimestamp() {
assertEquals(useAuthorName ? 1363875404000L : 1364213939000L, changeSet.getTimestamp());
}

@Test
public void testHashCode() {
assertTrue(changeSet.hashCode() != 0);
Expand Down
147 changes: 147 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetPluginHistoryTest.java
@@ -0,0 +1,147 @@
package hudson.plugins.git;

import org.jenkinsci.plugins.gitclient.Git;
import org.jenkinsci.plugins.gitclient.GitClient;

import org.eclipse.jgit.lib.ObjectId;

import hudson.EnvVars;
import hudson.FilePath;
import hudson.model.TaskListener;
import hudson.util.StreamTaskListener;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class GitChangeSetPluginHistoryTest {

private static final long FIRST_COMMIT_TIMESTAMP = 1198029565000L;
private static final long NOW = System.currentTimeMillis();

private final GitClient git;
private final boolean authorOrCommitter;
private final ObjectId sha1;

private final GitChangeSet changeSet;

private static String gitVersion = "Unknown";

/* git 1.7.1 on CentOS 6.7 "whatchanged" generates no output for
* the SHA1 hashes (from this repository) in this list. Rather
* than skip testing on that old git version, this exclusion list
* allows most tests to run.
*/
private static final String[] git171exceptions = {
"750b6806",
"7eeb070b",
"87988f4d",
"a571899e",
"bc71cd2d"
};

public GitChangeSetPluginHistoryTest(GitClient git, boolean authorOrCommitter, String sha1String) throws IOException, InterruptedException {
this.git = git;
this.authorOrCommitter = authorOrCommitter;
this.sha1 = ObjectId.fromString(sha1String);
StringWriter stringWriter = new StringWriter();
git.changelog().includes(sha1).max(1).to(stringWriter).execute();
List<String> changeLogStrings = new ArrayList<String>(Arrays.asList(stringWriter.toString().split("\n")));
changeSet = new GitChangeSet(changeLogStrings, authorOrCommitter);
}

private static String getGitVersion() throws IOException {
Process process = new ProcessBuilder("git", "--version").start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String version = "unknown";
String line;
while ((line = reader.readLine()) != null) {
version = line.trim();
}
reader.close();
process.destroy();
return version;
}

/**
* Merge changes won't compute their date in GitChangeSet,
* apparently as an intentional design choice. Return all changes
* for this repository which are not merges.
*
* @return ObjectId list for all changes which aren't merges
*/
private static List<ObjectId> getNonMergeChanges(boolean honorExclusions) throws IOException {
List<ObjectId> nonMergeChanges = new ArrayList<ObjectId>();
Process process = new ProcessBuilder("git", "rev-list", "--no-merges", "HEAD").start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (honorExclusions) {
boolean ignore = false;
for (String exclusion : git171exceptions) {
if (line.startsWith(exclusion)) {
ignore = true;
break;
}
}
if (!ignore) {
nonMergeChanges.add(ObjectId.fromString(line));
}
} else {
nonMergeChanges.add(ObjectId.fromString(line));
}
}
reader.close();
process.destroy();
Collections.shuffle(nonMergeChanges);
return nonMergeChanges;
}

@Parameterized.Parameters(name = "{2}-{1}")
public static Collection<Object[]> generateData() throws IOException, InterruptedException {
gitVersion = getGitVersion();

List<Object[]> args = new ArrayList<Object[]>();
String[] implementations = new String[]{"git", "jgit"};
boolean[] choices = {true, false};

for (final String implementation : implementations) {
EnvVars envVars = new EnvVars();
TaskListener listener = StreamTaskListener.fromStdout();
GitClient git = Git.with(listener, envVars).in(new FilePath(new File("."))).using(implementation).getClient();
List<ObjectId> allNonMergeChanges = getNonMergeChanges(gitVersion.equals("git version 1.7.1") && implementation.equals("git"));
int count = allNonMergeChanges.size() / 10; /* 10% of all changes */
for (boolean authorOrCommitter : choices) {
for (int index = 0; index < count; index++) {
ObjectId sha1 = allNonMergeChanges.get(index);
Object[] argList = {git, authorOrCommitter, sha1.getName()};
args.add(argList);
}
}
}
return args;
}

@Test
public void timestampInRange() {
long timestamp = changeSet.getTimestamp();
assertThat(timestamp, is(greaterThanOrEqualTo(FIRST_COMMIT_TIMESTAMP)));
assertThat(timestamp, is(lessThan(NOW)));
}
}
5 changes: 5 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetSimpleTest.java
Expand Up @@ -119,6 +119,11 @@ public void testGetAuthorName() {
assertEquals(useAuthorName ? GitChangeSetUtil.AUTHOR_NAME : GitChangeSetUtil.COMMITTER_NAME, changeSet.getAuthorName());
}

@Test
public void testGetDate() {
assertEquals(useAuthorName ? GitChangeSetUtil.AUTHOR_DATE_FORMATTED : GitChangeSetUtil.COMMITTER_DATE_FORMATTED, changeSet.getDate());
}

@Test
public void testGetMsg() {
assertEquals(GitChangeSetUtil.COMMIT_TITLE, changeSet.getMsg());
Expand Down
50 changes: 50 additions & 0 deletions src/test/java/hudson/plugins/git/GitChangeSetTimestampTest.java
@@ -0,0 +1,50 @@
package hudson.plugins.git;

import java.util.ArrayList;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;

/**
* JENKINS-30073 reports that the timestamp returns -1 for the typical timestamp
* reported by the +%ci format to git log and git whatchanged. This test
* duplicates the bug.
*
* @author Mark Waite
*/
public class GitChangeSetTimestampTest {

private GitChangeSet changeSet = null;

@Before
public void createChangeSet() {
changeSet = genChangeSetForJenkins30073(true);
}

@Test
public void testChangeSetDate() {
assertEquals("2015-10-06 19:29:47 +0300", changeSet.getDate());
}

@Test
@Issue("JENKINS-30073")
public void testChangeSetTimeStamp() {
assertEquals(1444148987000L, changeSet.getTimestamp());
}

private GitChangeSet genChangeSetForJenkins30073(boolean authorOrCommitter) {
ArrayList<String> lines = new ArrayList<String>();
lines.add("commit 302548f75c3eb6fa1db83634e4061d0ded416e5a");
lines.add("tree e1bd430d3f45b7aae54a3061b7895ee1858ec1f8");
lines.add("parent c74f084d8f9bc9e52f0b3fe9175ad27c39947a73");
lines.add("author Viacheslav Kopchenin <vkopchenin@odin.com> 2015-10-06 19:29:47 +0300");
lines.add("committer Viacheslav Kopchenin <vkopchenin@odin.com> 2015-10-06 19:29:47 +0300");
lines.add("");
lines.add(" pom.xml");
lines.add(" ");
lines.add(" :100644 100644 bb32d78c69a7bf79849217bc02b1ba2c870a5a66 343a844ad90466d8e829896c1827ca7511d0d1ef M modules/platform/pom.xml");
lines.add("");
return new GitChangeSet(lines, authorOrCommitter);
}
}

0 comments on commit 1eda492

Please sign in to comment.