001/* 002 * JDrupes GitVersioning 003 * Copyright (C) 2025 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.gitversioning.core; 020 021import com.vdurmont.semver4j.Semver; 022import com.vdurmont.semver4j.SemverException; 023import java.io.IOException; 024import java.nio.file.Path; 025import java.util.Collections; 026import java.util.Comparator; 027import java.util.HashSet; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.function.Consumer; 033import java.util.logging.Logger; 034import org.eclipse.jgit.api.Git; 035import org.eclipse.jgit.api.errors.GitAPIException; 036import org.eclipse.jgit.lib.ObjectId; 037import org.eclipse.jgit.lib.Ref; 038import org.eclipse.jgit.lib.Repository; 039import org.eclipse.jgit.revwalk.RevCommit; 040import org.eclipse.jgit.revwalk.RevObject; 041import org.eclipse.jgit.revwalk.RevTag; 042import org.eclipse.jgit.revwalk.RevWalk; 043import org.jdrupes.gitversioning.api.TagFilter; 044import org.jdrupes.gitversioning.api.TagProcessor; 045import org.jdrupes.gitversioning.api.VersionEvaluator; 046 047/** 048 * A simple version evaluator provider. It looks up the tags in the 049 * current branch, uses the tag filter to identify the version tags 050 * and selects the latest one. 051 * 052 * It then invokes the tag processor to produce a version string. 053 */ 054public class VersionEvaluatorProvider 055 implements org.jdrupes.gitversioning.api.VersionEvaluatorProvider { 056 057 /** The logger. */ 058 protected final Logger log = Logger.getLogger(getClass().getName()); 059 @SuppressWarnings("PMD.FieldNamingConventions") 060 private static final Map<ObjectId, Set<ObjectId>> reachableByHead 061 = new ConcurrentHashMap<>(); 062 private Repository repository; 063 private Path directory; 064 private TagFilter tagFilter = new DefaultTagFilter(); 065 private TagProcessor tagProcessor = new MavenStyleTagProcessor(); 066 067 /** 068 * Initializes a new version evaluator provider. 069 */ 070 public VersionEvaluatorProvider() { 071 // Make javadoc happy. 072 } 073 074 @Override 075 public VersionEvaluatorProvider repository(Repository repository) { 076 this.repository = repository; 077 return this; 078 } 079 080 @Override 081 public VersionEvaluator subDirectory(Path subDirectory) { 082 if (subDirectory.isAbsolute()) { 083 subDirectory = subDirectory.relativize( 084 repository.getWorkTree().toPath()); 085 } 086 directory = subDirectory; 087 return this; 088 } 089 090 @Override 091 public VersionEvaluator tagFilter(TagFilter tagFilter) { 092 this.tagFilter = tagFilter; 093 return this; 094 } 095 096 @Override 097 public VersionEvaluator tagProcessor(TagProcessor tagProcessor) { 098 this.tagProcessor = tagProcessor; 099 return this; 100 } 101 102 @Override 103 public String version() { 104 try { 105 var latest = getLatestVersionTagged(); 106 return tagProcessor.version(repository, directory, 107 latest.commit(), latest.tag(), latest.version().toString()); 108 } catch (IOException | GitAPIException e) { 109 throw new IllegalStateException(e); 110 } 111 } 112 113 private record VersionedTag(Ref ref, String tag, Semver version) { 114 } 115 116 private record VersionedCommit(RevCommit commit, String tag, 117 Semver version) { 118 } 119 120 private VersionedCommit getLatestVersionTagged() 121 throws GitAPIException, IOException { 122 try (var git = Git.wrap(repository); 123 var revWalk = new RevWalk(repository)) { 124 var reachable = reachableCommits(); 125 return git.tagList().call().stream() 126 .mapMulti((Ref ref, Consumer< 127 VersionedTag> consumer) -> addVersionInfo(ref) 128 .ifPresent(consumer)) 129 .sorted(new Comparator<VersionedTag>() { 130 @Override 131 public int compare(VersionedTag obj1, VersionedTag obj2) { 132 return obj2.version().compareTo(obj1.version()); 133 } 134 }).mapMulti((VersionedTag vt, 135 Consumer<VersionedCommit> consumer) -> findCommit( 136 revWalk, vt.ref()).ifPresent( 137 c -> consumer.accept(new VersionedCommit( 138 c, vt.tag(), vt.version())))) 139 .filter(vc -> reachable.contains(vc.commit().getId())) 140 .findFirst().orElseGet( 141 () -> new VersionedCommit(null, null, new Semver("0.0.0"))); 142 } 143 } 144 145 private Optional<RevCommit> findCommit(RevWalk revWalk, Ref ref) { 146 try { 147 RevObject refd = revWalk.parseAny(ref.getObjectId()); 148 revWalk.reset(); 149 return switch (refd) { 150 case RevTag revtag -> Optional 151 .of(revWalk.parseCommit(revtag.getObject())); 152 case RevCommit revcommit -> Optional.of(revcommit); 153 default -> Optional.empty(); 154 }; 155 } catch (IOException e) { 156 return Optional.empty(); 157 } finally { 158 revWalk.reset(); 159 } 160 } 161 162 private Set<ObjectId> reachableCommits() throws IOException { 163 ObjectId headId = repository.resolve("HEAD"); 164 if (headId == null) { 165 // No commits yet 166 return Collections.emptySet(); 167 } 168 return reachableByHead.computeIfAbsent( 169 headId, k -> { 170 try (var revWalk = new RevWalk(repository)) { 171 var reachable = new HashSet<ObjectId>(); 172 revWalk.markStart(revWalk.parseCommit(headId)); 173 for (RevCommit commit : revWalk) { 174 reachable.add(commit.getId()); 175 } 176 return reachable; 177 } catch (IOException e) { 178 return Collections.emptySet(); 179 } 180 }); 181 } 182 183 private Optional<VersionedTag> addVersionInfo(Ref ref) { 184 var tag = ref.getName().substring("refs/tags/".length()); 185 return tagFilter.version(tag).map(v -> { 186 try { 187 var version = new Semver(v, Semver.SemverType.LOOSE); 188 return new VersionedTag(ref, tag, version); 189 } catch (SemverException e) { 190 throw new IllegalArgumentException( 191 "Failed to parse version: " + v, e); 192 } 193 }); 194 } 195 196}