diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/domain/VersionResolver.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/domain/VersionResolver.groovy index 77412b4d..2319953d 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/domain/VersionResolver.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/domain/VersionResolver.groovy @@ -7,6 +7,7 @@ import pl.allegro.tech.build.axion.release.domain.properties.VersionProperties import pl.allegro.tech.build.axion.release.domain.scm.ScmPosition import pl.allegro.tech.build.axion.release.domain.scm.ScmRepository import pl.allegro.tech.build.axion.release.domain.scm.TagsOnCommit +import pl.allegro.tech.build.axion.release.infrastructure.git.ScmCache; import java.util.regex.Pattern @@ -46,7 +47,7 @@ class VersionResolver { versions.onReleaseTag, versions.onNextVersionTag, versions.noTagsFound, - repository.checkUncommittedChanges() + ScmCache.getInstance().checkUncommittedChanges(repository) ) Map finalVersion = versionFactory.createFinalVersion(scmState, versions.current) diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/domain/scm/ScmRepository.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/domain/scm/ScmRepository.groovy index 25577b62..b61e61ef 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/domain/scm/ScmRepository.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/domain/scm/ScmRepository.groovy @@ -4,6 +4,8 @@ import java.util.regex.Pattern interface ScmRepository { + String id(); + void fetchTags(ScmIdentity identity, String remoteName) void tag(String tagName) diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DryRepository.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DryRepository.groovy index 57a9f8e8..4c1a3ae2 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DryRepository.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DryRepository.groovy @@ -20,6 +20,11 @@ class DryRepository implements ScmRepository { this.delegateRepository = delegateRepository } + @Override + String id() { + return delegateRepository.id(); + } + @Override void fetchTags(ScmIdentity identity, String remoteName) { log("fetching tags from remote") diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DummyRepository.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DummyRepository.groovy index 1d9d1188..40a13a9f 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DummyRepository.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/DummyRepository.groovy @@ -21,6 +21,11 @@ class DummyRepository implements ScmRepository { logger.quiet("Couldn't perform $commandName command on uninitialized repository") } + @Override + String id() { + return "DummyRepository" + } + @Override void fetchTags(ScmIdentity identity, String remoteName) { log('fetch tags') diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepository.groovy b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepository.groovy index 9bf9d89a..5ac3e956 100644 --- a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepository.groovy +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepository.groovy @@ -9,6 +9,7 @@ import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevSort import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.transport.* +import org.eclipse.jgit.util.FS import pl.allegro.tech.build.axion.release.domain.logging.ReleaseLogger import pl.allegro.tech.build.axion.release.domain.scm.* @@ -31,7 +32,8 @@ class GitRepository implements ScmRepository { GitRepository(ScmProperties properties) { try { this.repositoryDir = properties.directory - this.jgitRepository = Git.open(repositoryDir) + RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(repositoryDir, FS.DETECTED); + this.jgitRepository = Git.wrap(RepositoryCache.open(key, true)); this.properties = properties } catch (RepositoryNotFoundException exception) { @@ -46,6 +48,11 @@ class GitRepository implements ScmRepository { } } + @Override + public String id() { + return repositoryDir.getAbsolutePath(); + } + /** * This fetch method behaves like git fetch, meaning it only fetches thing without merging. * As a result, any fetched tags will not be visible via GitRepository tag listing methods @@ -74,6 +81,7 @@ class GitRepository implements ScmRepository { jgitRepository.tag() .setName(tagName) .call() + ScmCache.getInstance().invalidate(this) } else { logger.debug("The head commit $headId already has the tag $tagName.") } @@ -89,6 +97,7 @@ class GitRepository implements ScmRepository { jgitRepository.tagDelete() .setTags(GIT_TAG_PREFIX + tagName) .call() + ScmCache.getInstance().invalidate(this) } catch (GitAPIException e) { throw new ScmException(e) } @@ -117,7 +126,9 @@ class GitRepository implements ScmRepository { private Iterable callPush(PushCommand pushCommand) { try { - return pushCommand.call() + def result = pushCommand.call() + ScmCache.getInstance().invalidate(this); + return result } catch (GitAPIException e) { throw new ScmException(e) } @@ -174,6 +185,7 @@ class GitRepository implements ScmRepository { jgitRepository.commit() .setMessage(message) .call() + ScmCache.getInstance().invalidate(this) } ScmPosition currentPosition() { @@ -242,25 +254,33 @@ class GitRepository implements ScmRepository { } RevWalk walk = walker(startingCommit) - if (!inclusive) { - walk.next() - } - - Map> allTags = tagsMatching(pattern, walk) - - RevCommit currentCommit - List currentTagsList - for (currentCommit = walk.next(); currentCommit != null; currentCommit = walk.next()) { - currentTagsList = allTags[currentCommit.id.name] - if (currentTagsList) { - TagsOnCommit taggedCommit = new TagsOnCommit(currentCommit.id.name(), currentTagsList, Objects.equals(currentCommit.id, headId)) - taggedCommits.add(taggedCommit) - if (stopOnFirstTag) { - break + try { + if (!inclusive) { + walk.next(); + } + Map> allTags = tagsMatching(pattern, walk) + if (stopOnFirstTag) { + // stopOnFirstTag needs to get latest tag, therefore the order does matter + // order is given by walking the repository commits. this can be slower in some + // situations than returning all tagged commits + RevCommit currentCommit; + for (currentCommit = walk.next(); currentCommit != null; currentCommit = walk.next()) { + List tagList = allTags.get(currentCommit.getId().getName()); + if (tagList != null) { + TagsOnCommit taggedCommit = new TagsOnCommit(currentCommit.getId().name(), tagList, + headId.name().equals(currentCommit.getId().name())) + taggedCommits.add(taggedCommit); + break; + } } + } else { + // order not needed, we can just return all tagged commits + allTags.each {it -> taggedCommits + .add(new TagsOnCommit(it.getKey(), it.getValue(), headId.name().equals(it.getKey())))} } + } finally { + walk.dispose(); } - walk.dispose() return taggedCommits } @@ -278,14 +298,43 @@ class GitRepository implements ScmRepository { } private Map> tagsMatching(Pattern pattern, RevWalk walk) { - List tags = jgitRepository.tagList().call() + List> parsedTagList = ScmCache.getInstance().parsedTagList(this, jgitRepository, walk); + + Map> tags = new HashMap<>() + parsedTagList + .collect {pair -> new TagNameAndId( + pair.getFirst().getName().substring(GIT_TAG_PREFIX.length()), + pair.getSecond().getName()) + }.findAll { t -> pattern.matcher(t.name).matches() } + .each { + def list = tags.get(it.id) + if (list == null) { + list = new ArrayList() + tags.put(it.id, list) + } + list.add(it.name) + } + return tags - .collect({ tag -> [id: walk.parseCommit(tag.objectId).name, name: tag.name.substring(GIT_TAG_PREFIX.length())] }) - .grep({ tag -> tag.name ==~ pattern }) - .inject([:].withDefault({ p -> [] }), { map, entry -> - map[entry.id] << entry.name - return map - }) +// return parsedTagList.stream() +// .map(pair -> new TagNameAndId( +// pair.getFirst().getName().substring(GIT_TAG_PREFIX.length()), +// pair.getSecond().getName())) +// .filter(t -> pattern.matcher(t.name).matches()) +// .collect( +// HashMap::new, +// (m, t) -> m.computeIfAbsent(t.id, (s) -> new ArrayList<>()).add(t.name), +// HashMap::putAll); + } + + private final static class TagNameAndId { + final String name; + final String id; + + TagNameAndId(String name, String id) { + this.name = name; + this.id = id; + } } private boolean hasCommits() { diff --git a/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/ScmCache.java b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/ScmCache.java new file mode 100644 index 00000000..afabe932 --- /dev/null +++ b/src/main/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/ScmCache.java @@ -0,0 +1,106 @@ +package pl.allegro.tech.build.axion.release.infrastructure.git; + +import groovy.lang.Tuple2; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import pl.allegro.tech.build.axion.release.domain.scm.ScmException; +import pl.allegro.tech.build.axion.release.domain.scm.ScmRepository; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Provides cached version for some operations on {@link ScmRepository} + */ +public class ScmCache { + + /** + * Since this cache is statis and Gradle Demon might keep JVM process in background for a long + * time, we have to put some TTL for cached values. + */ + private static final long INVALIDATE_AFTER_MILLIS = 1000 * 60; + + private static final ScmCache CACHE = new ScmCache(); + + public static ScmCache getInstance() { + return CACHE; + } + + private ScmCache() { } + + private final Map cache = new HashMap<>(); + + synchronized void invalidate(ScmRepository repository) { + cache.remove(repository.id()); + } + + public synchronized boolean checkUncommittedChanges(ScmRepository repository) { + CachedState state = retrieveCachedStateFor(repository); + if (state.hasUncommittedChanges == null) { + state.hasUncommittedChanges = repository.checkUncommittedChanges(); + } + return state.hasUncommittedChanges; + } + + synchronized List> parsedTagList(ScmRepository repository, Git git, RevWalk walker) throws GitAPIException { + CachedState state = retrieveCachedStateFor(repository); + if (state.tags == null) { + List> list = new ArrayList<>(); + for (Ref tag : git.tagList().call()) { + try { + list.add(new Tuple2<>(tag, walker.parseCommit(tag.getObjectId()))); + } catch (IOException e) { + throw new ScmException(e); + } + } + state.tags = list; + } + return state.tags; + } + + private CachedState retrieveCachedStateFor(ScmRepository scmRepository) { + String key = scmRepository.id(); + String currentHeadRevision = scmRepository.currentPosition().getRevision(); + long currentTime = System.currentTimeMillis(); + CachedState state = cache.get(key); + if (state == null) { + state = new CachedState(currentHeadRevision); + cache.put(key, state); + } else { + if (!currentHeadRevision.equals(state.headRevision) || (state.createTimestamp + INVALIDATE_AFTER_MILLIS) < currentTime) { + state = new CachedState(currentHeadRevision); + cache.put(key, state); + } + } + return state; + } + + /** + * Helper object holding cached values per SCM repository + */ + private static class CachedState { + + final long createTimestamp; + + /** + * HEAD revision of repo for which this cache was created. Cache has to be invalidated + * if HEAD changes. + */ + final String headRevision; + + Boolean hasUncommittedChanges; + + List> tags; + + CachedState(String headRevision) { + createTimestamp = System.currentTimeMillis(); + this.headRevision = headRevision; + } + } +} diff --git a/src/test/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepositoryTest.groovy b/src/test/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepositoryTest.groovy index 8b29e75a..0aa2e577 100644 --- a/src/test/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepositoryTest.groovy +++ b/src/test/groovy/pl/allegro/tech/build/axion/release/infrastructure/git/GitRepositoryTest.groovy @@ -171,7 +171,8 @@ class GitRepositoryTest extends Specification { List allTaggedCommits = repository.taggedCommits(~/^release.*/) then: - allTaggedCommits.collect { c -> c.tags[0] } == ['release-3', 'release-4', 'release-2', 'release-1'] + allTaggedCommits.collect { c -> c.tags[0] }.toSet() == + ['release-3', 'release-4', 'release-2', 'release-1'].toSet() } def "should return only tags that match with prefix"() {