com.exasol.projectkeeper.shared.repository.GitRepository Maven / Gradle / Ivy
The newest version!
package com.exasol.projectkeeper.shared.repository;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.revwalk.*;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.treewalk.TreeWalk;
import com.exasol.errorreporting.ExaError;
import com.exasol.projectkeeper.shared.ExasolVersionMatcher;
/**
* This gives access to a local git repository.
*/
public class GitRepository implements AutoCloseable {
private final Path projectDirectory;
private final Git git;
private GitRepository(final Git git, final Path projectDirectory) {
this.git = git;
this.projectDirectory = projectDirectory;
}
/**
* Open the given git repository and create a new instance of {@link GitRepository}. Close the repository after use
* with {@link #close()}.
*
* @param projectDirectory the projects directory.
* @return an open git repository.
*/
public static GitRepository open(final Path projectDirectory) {
return new GitRepository(openLocalGitRepository(projectDirectory), projectDirectory);
}
private static Git openLocalGitRepository(final Path projectDirectory) {
try {
return Git.open(projectDirectory.toFile());
} catch (final IOException exception) {
throw new IllegalStateException(ExaError.messageBuilder("E-PK-SMC-32")
.message("Failed to open local git repository {{repository}}.")
.mitigation("If this is a new project you maybe need to create the git project using `git init`.")
.parameter("repository", projectDirectory.toString()).toString(), exception);
}
}
/**
* Find the latest release commit matching an Exasol version (see
* {@link ExasolVersionMatcher#isExasolStyleVersion(String)}). If {@code currentVersion} is not {@code null}, this
* version will be ignored if a tag exists.
*
* @param currentVersion a version number to ignore or {@code null}
* @return the latest release commit
*/
public Optional findLatestReleaseCommit(final String currentVersion) {
final var exasolVersionMatcher = new ExasolVersionMatcher();
return this.getTagsInCurrentBranch().stream()
.filter(taggedCommit -> exasolVersionMatcher.isExasolStyleVersion(taggedCommit.getTag()))//
.filter(taggedCommit -> !taggedCommit.getTag().equals(currentVersion))//
.findFirst();
}
/**
* Get the tags of the current branch ordered descending by commit date. If one commit has multiple tags, they are
* returned sequential with no specified order.
*
* @return descending ordered list of tags
*/
List getTagsInCurrentBranch() {
try {
final String currentBranch = this.git.getRepository().getFullBranch();
validateBranchExists(currentBranch);
return getTagsInBranch(currentBranch);
} catch (final IOException exception) {
throw new IllegalStateException(
ExaError.messageBuilder("E-PK-SMC-33")
.message("Failed to retrieve latest tag from the local git repository.").toString(),
exception);
}
}
private void validateBranchExists(final String currentBranch) {
if (currentBranch == null) {
throw new IllegalStateException(
ExaError.messageBuilder("E-PK-SMC-37").message("Could not get checked out branch of repository.")
.mitigation("Create a branch and check it out.").toString());
}
}
private List getTagsInBranch(final String branchName) throws IOException {
final Repository repo = this.git.getRepository();
final ObjectId branch = repo.resolve(branchName);
final Map> tags = getTagsByTheIdOfTheCommitTheyPointTo(repo);
try (final var commitWalker = new RevWalk(repo)) {
try {
commitWalker.markStart(commitWalker.parseCommit(branch));
} catch (final NullPointerException exception) {
// NullPointerException is thrown if the branch has no commits
return Collections.emptyList();
}
commitWalker.sort(RevSort.COMMIT_TIME_DESC);
return readTagsFromCommits(tags, commitWalker);
}
}
private List readTagsFromCommits(final Map> tags, final RevWalk commitWalker) {
final List result = new ArrayList<>();
for (final RevCommit commit : commitWalker) {
if (tags.containsKey(commit.getId())) {
final List commitsTags = tags.get(commit.getId());
for (final String tag : commitsTags) {
result.add(new TaggedCommit(commit, tag));
}
}
}
return result;
}
private Map> getTagsByTheIdOfTheCommitTheyPointTo(final Repository repository)
throws IOException {
final Map> taggedResources = new HashMap<>();
for (final Ref tagRef : repository.getRefDatabase().getRefsByPrefix(Constants.R_TAGS)) {
final String tagName = tagRef.getName().replace(Constants.R_TAGS, "");
final ObjectId commitId = getTaggedCommitId(repository, tagRef);
taggedResources.computeIfAbsent(commitId, commit -> new LinkedList<>()).add(tagName);
}
return taggedResources;
}
private ObjectId getTaggedCommitId(final Repository repository, final Ref tagRef) throws IOException {
try (final var tagSearcher = new RevWalk(repository)) {
final var revObject = tagSearcher.parseAny(tagRef.getObjectId());
if (revObject instanceof RevCommit) {
return revObject.getId();
} else if (revObject instanceof RevTag) {
return ((RevTag) revObject).getObject().getId();
} else {
throw new UnsupportedOperationException(ExaError.messageBuilder("F-PK-SMC-44")
.message("Unsupported tag target {{target class name}}.")
.parameter("target class name", revObject.getClass().getName()).ticketMitigation().toString());
}
}
}
/**
* Read a specific file at a given commit and extract it to a target file.
*
* @param relativeFilePath file path
* @param commit commit
* @param targetFile path where this method writes the content to
* @throws FileNotFoundException if the file does not exist in the repo
*/
public void extractFileFromCommit(final Path relativeFilePath, final GitCommit commit, final Path targetFile)
throws FileNotFoundException {
try {
extractFileFromCommit(relativeFilePath, this.git, commit.getCommit(), targetFile);
} catch (final FileNotFoundException exception) {
throw exception;
} catch (final IOException exception) {
throw new IllegalStateException(ExaError.messageBuilder("E-PK-SMC-43").message(
"Failed to copy file {{path}} from git repo {{git repo}} at commit {{commit}} to {{target file}}.",
relativeFilePath, this.projectDirectory, commit.getCommit(), targetFile).toString(), exception);
}
}
/**
* Read a specific file at a given commit and return its content.
*
* @param relativeFilePath file path
* @param commit commit
* @return the file content
* @throws FileNotFoundException if the file does not exist in the repo
*/
public String getFileFromCommit(final Path relativeFilePath, final GitCommit commit) throws FileNotFoundException {
try {
final var repository = this.git.getRepository();
final ObjectId objectForVersionOfFile = findFile(commit.getCommit(), repository, relativeFilePath);
try (final var reader = repository.newObjectReader()) {
final var objectLoader = reader.open(objectForVersionOfFile);
return new String(objectLoader.getBytes(), StandardCharsets.UTF_8);
}
} catch (final FileNotFoundException exception) {
throw exception;
} catch (final IOException exception) {
throw new IllegalStateException(ExaError.messageBuilder("E-PK-SMC-80")
.message("Failed to retrieve file {{path}} from git repo {{git repo}} at commit {{commit}}.",
relativeFilePath, this.projectDirectory, commit.getCommit())
.toString(), exception);
}
}
private void extractFileFromCommit(final Path relativeFilePath, final Git git, final RevCommit commit,
final Path targetFile) throws IOException {
final var repository = git.getRepository();
final ObjectId objectForVersionOfFile = findFile(commit, repository, relativeFilePath);
copyVersionOfFile(repository, objectForVersionOfFile, targetFile);
}
private void copyVersionOfFile(final Repository repository, final ObjectId objectForVersionOfFile,
final Path targetFile) throws IOException {
final Path targetDir = targetFile.getParent();
if (!Files.exists(targetDir)) {
Files.createDirectories(targetDir);
}
try (final var reader = repository.newObjectReader();
final FileOutputStream outputStream = new FileOutputStream(targetFile.toFile());
final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
final var objectLoader = reader.open(objectForVersionOfFile);
objectLoader.copyTo(bufferedOutputStream);
}
}
private ObjectId findFile(final RevCommit commit, final Repository repository, final Path expectedPath)
throws IOException {
final Path normalizedExpected = expectedPath.normalize();
try (final TreeWalk treeWalk = new TreeWalk(repository)) {
treeWalk.addTree(commit.getTree());
treeWalk.setRecursive(true);
while (treeWalk.next()) {
final Path currentPath = Path.of(treeWalk.getPathString()).normalize();
if (treeWalk.isSubtree()) {
if (normalizedExpected.startsWith(currentPath)) {
treeWalk.enterSubtree();
}
} else {
if (normalizedExpected.equals(currentPath)) {
return treeWalk.getObjectId(0);
}
}
}
}
throw new FileNotFoundException(ExaError.messageBuilder("E-PK-SMC-35")
.message("Failed to read file {{file path}} from commit {{commit id}}.")
.parameter("file path", expectedPath).parameter("commit id", commit.getName())
.mitigation("Make sure that the file exists at the given commit.").toString());
}
/**
* Try to get a the name of the repository from the git remote configuration.
*
* @return repository name
*/
public Optional getRepoNameFromRemote() {
try {
final List remotes = this.git.remoteList().call();
final Optional origin = remotes.stream().filter(remote -> remote.getName().equals("origin"))
.findAny();
if (origin.isPresent()) {
final String path = origin.get().getURIs().get(0).getPath();
final String[] pathParts = path.split("/");
final String repoName = pathParts[pathParts.length - 1].replace(".git", "");
return Optional.of(repoName);
} else {
return Optional.empty();
}
} catch (final Exception exception) {
return Optional.empty();
}
}
@Override
public void close() {
this.git.close();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy