Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
Expand Down Expand Up @@ -425,6 +428,41 @@
}
}

private long getTagTimestamp(RevWalk walk, ObjectId objectId) throws IOException {
try {
RevObject target = walk.parseAny(objectId);

// If the first hash is an annotated tag, prefer its tag time
if (target instanceof RevTag) {

Check warning on line 436 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 436 is only partially covered, one branch is missing
RevTag tag = walk.parseTag((RevTag) target);
PersonIdent tagger = tag.getTaggerIdent();
if (tagger != null && tagger.getWhen() != null) {
return tagger.getWhen().getTime();
}
target = tag.getObject(); // walk to commit if needed

Check warning on line 442 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 437-442 are not covered by tests
}

// Walk until we reach a commit (or give up)
target = walk.parseAny(target);
for (int i = 0; i < 32 && !(target instanceof RevCommit); i++) { //32 is to guard against inf loop

Check warning on line 447 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 447 is only partially covered, 2 branches are missing
if (!(target instanceof RevTag)) break;
RevTag tag = walk.parseTag((RevTag) target);
target = walk.parseAny(tag.getObject());

Check warning on line 450 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 448-450 are not covered by tests
}

if (target instanceof RevCommit) {

Check warning on line 453 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 453 is only partially covered, one branch is missing
return TimeUnit.SECONDS.toMillis(((RevCommit) target).getCommitTime());
}

throw new IOException("Tag does not ultimately reference a commit: " + objectId.name());
} catch (org.eclipse.jgit.errors.IncorrectObjectTypeException e) {
// Lightweight tag (or direct commit id)
RevCommit commit = walk.parseCommit(objectId);
return TimeUnit.SECONDS.toMillis(commit.getCommitTime());

Check warning on line 461 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 457-461 are not covered by tests
}
}


/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -786,8 +824,7 @@
}
count++;
final String tagName = StringUtils.removeStart(ref.getKey(), Constants.R_TAGS);
RevCommit commit = walk.parseCommit(ref.getValue());
final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime());
final long lastModified = getTagTimestamp(walk, ref.getValue());
if (request.process(new GitTagSCMHead(tagName, lastModified),
new SCMSourceRequest.IntermediateLambda<ObjectId>() {
@Nullable
Expand All @@ -804,7 +841,7 @@
@Nullable ObjectId revisionInfo)
throws IOException, InterruptedException {
RevCommit commit = walk.parseCommit(revisionInfo);
final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime());
final long lastModified = getTagTimestamp(walk, revisionInfo);
final RevTree tree = commit.getTree();
return new TreeWalkingSCMProbe(tagName, lastModified, repository, tree);
}
Expand Down Expand Up @@ -1012,8 +1049,7 @@
final Repository repository = client.getRepository();
RevWalk walk = new RevWalk(repository)) {
ObjectId ref = client.revParse(tagRef);
RevCommit commit = walk.parseCommit(ref);
long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime());
long lastModified = getTagTimestamp(walk, ref);
listener.getLogger().printf("Resolved tag %s revision %s%n", revision,
ref.getName());
return new GitTagSCMRevision(new GitTagSCMHead(revision, lastModified),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import hudson.EnvVars;
import hudson.model.TaskListener;
Expand Down Expand Up @@ -62,6 +66,8 @@ static void beforeAll(GitSampleRepoRule repo) throws Exception {
sampleRepo.write("file", "modified");
sampleRepo.git("commit", "--all", "--message=" + BRANCH_NAME + "-commit-1");
sampleRepo.git("tag", LIGHTWEIGHT_TAG_NAME);
// Sleep to ensure annotated tag has a different timestamp than lightweight tag
Thread.sleep(1100);
sampleRepo.write("file", "modified2");
sampleRepo.git("commit", "--all", "--message=" + BRANCH_NAME + "-commit-2");
sampleRepo.git("tag", "-a", ANNOTATED_TAG_NAME, "-m", "annotated-tag-message");
Expand Down Expand Up @@ -169,6 +175,144 @@ void indexingHasBranchAndTagDiscoveryTraitIgnoreTagDiscoveryTrait() throws Excep
assertTrue(tagsFetched);
}

@Test
public void tagTimestampsAreValid() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

Set<GitTagSCMHead> tags = heads.stream()
.filter(h -> h instanceof GitTagSCMHead)
.map(h -> (GitTagSCMHead) h)
.collect(Collectors.toSet());

assertThat("Should discover both tags", tags.size(), is(2));

long year2000 = 946684800000L; // Jan 1, 2000
for (GitTagSCMHead tag : tags) {
assertThat("Tag " + tag.getName() + " should have valid timestamp",
tag.getTimestamp(), greaterThan(year2000));
}
}

@Test
public void lightweightTagHasCommitTimestamp() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

GitTagSCMHead lightweightTag = heads.stream()
.filter(h -> h instanceof GitTagSCMHead && h.getName().equals(LIGHTWEIGHT_TAG_NAME))
.map(h -> (GitTagSCMHead) h)
.findFirst()
.orElse(null);

assertNotNull(lightweightTag, "Lightweight tag should be discovered");
assertThat("Lightweight tag should have a timestamp", lightweightTag.getTimestamp(), greaterThan(0L));
}

@Test
public void annotatedTagHasValidTimestamp() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

GitTagSCMHead annotatedTag = heads.stream()
.filter(h -> h instanceof GitTagSCMHead && h.getName().equals(ANNOTATED_TAG_NAME))
.map(h -> (GitTagSCMHead) h)
.findFirst()
.orElse(null);

assertNotNull(annotatedTag, "Annotated tag should be discovered");
assertThat("Annotated tag should have a timestamp", annotatedTag.getTimestamp(), greaterThan(0L));
}

@Test
public void lightweightAndAnnotatedTagsHaveDifferentCharacteristics() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

Set<GitTagSCMHead> tags = heads.stream()
.filter(h -> h instanceof GitTagSCMHead)
.map(h -> (GitTagSCMHead) h)
.collect(Collectors.toSet());

assertThat("Should discover both tags", tags.size(), is(2));

// Both tags should have valid timestamps
for (GitTagSCMHead tag : tags) {
long timestamp = tag.getTimestamp();
assertThat("Tag " + tag.getName() + " timestamp should be positive", timestamp, greaterThan(0L));
// Timestamps should be in milliseconds (modern times are > 1.5 billion ms since epoch)
assertThat("Tag " + tag.getName() + " timestamp should be in milliseconds",
timestamp, greaterThan(1500000000000L));
}
}

@Test
public void allDiscoveredTagsHaveValidTimestamps() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

Set<GitTagSCMHead> tags = heads.stream()
.filter(h -> h instanceof GitTagSCMHead)
.map(h -> (GitTagSCMHead) h)
.collect(Collectors.toSet());

assertThat("Should discover both tags", tags.size(), is(2));

// All tags should have timestamps that are:
// 1. Greater than year 2000 in milliseconds (946684800000)
// 2. Less than or equal to current time
long year2000Millis = 946684800000L;
long currentTimeMillis = System.currentTimeMillis();

for (GitTagSCMHead tag : tags) {
long timestamp = tag.getTimestamp();
assertThat("Tag " + tag.getName() + " timestamp should be after year 2000",
timestamp, greaterThan(year2000Millis));
assertThat("Tag " + tag.getName() + " timestamp should not be in the future",
timestamp, lessThanOrEqualTo(currentTimeMillis));
}
}

@Test
public void annotatedTagHasDifferentTimestampFromLightweightTag() throws Exception {
source.setTraits(Collections.singletonList(new TagDiscoveryTrait()));
Set<SCMHead> heads = source.fetch(LISTENER);

GitTagSCMHead lightweightTag = heads.stream()
.filter(h -> h instanceof GitTagSCMHead && h.getName().equals(LIGHTWEIGHT_TAG_NAME))
.map(h -> (GitTagSCMHead) h)
.findFirst()
.orElse(null);

GitTagSCMHead annotatedTag = heads.stream()
.filter(h -> h instanceof GitTagSCMHead && h.getName().equals(ANNOTATED_TAG_NAME))
.map(h -> (GitTagSCMHead) h)
.findFirst()
.orElse(null);

assertNotNull(lightweightTag, "Lightweight tag should be discovered");
assertNotNull(annotatedTag, "Annotated tag should be discovered");

long lightweightTimestamp = lightweightTag.getTimestamp();
long annotatedTimestamp = annotatedTag.getTimestamp();

// Lightweight tag uses the commit's timestamp (commit-1)
// Annotated tag uses the tagger's timestamp (when the tag was created, after a 1.1 second sleep)
// They should have different timestamps due to the sleep in beforeAll
assertFalse(
lightweightTimestamp == annotatedTimestamp,
"Annotated tag timestamp (" + annotatedTimestamp + ") should differ from " +
"lightweight tag timestamp (" + lightweightTimestamp + ") " +
"since annotated tag uses tagger's timestamp while lightweight uses commit timestamp"
);

// Annotated tag should be newer (created after the sleep)
assertThat(
"Annotated tag timestamp should be greater than lightweight tag timestamp",
annotatedTimestamp, greaterThan(lightweightTimestamp)
);
}

static boolean tagsFetched;

public static class MockGitClientForTags extends TestJGitAPIImpl {
Expand Down
Loading