DslGitRepository.java
package com.taxonomy.dsl.storage;
import com.taxonomy.dsl.diff.ModelDiff;
import com.taxonomy.dsl.diff.ModelDiffer;
import com.taxonomy.dsl.mapper.AstToModelMapper;
import com.taxonomy.dsl.model.CanonicalArchitectureModel;
import com.taxonomy.dsl.parser.TaxDslParser;
import io.github.carstenartur.jgit.storage.hibernate.HibernateGitStorage;
import io.github.carstenartur.jgit.storage.hibernate.HibernateRepositoryFactory;
import io.github.carstenartur.jgit.storage.hibernate.RepositoryName;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Taxonomy-specific facade around a JGit repository that versions one DSL document.
*
* <p>DSL file conventions, parsing, semantic diff and workflow operations remain
* application concerns. Persistent pack, ref and queryable reflog storage is
* delegated to {@code jgit-storage-hibernate-core} through its public facade.</p>
*/
public class DslGitRepository implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(DslGitRepository.class);
private static final String DEFAULT_REPOSITORY_NAME = "taxonomy-dsl";
/** The single DSL file stored in every commit tree. */
public static final String DSL_FILENAME = "architecture.taxdsl";
private final Repository gitRepo;
private final HibernateGitStorage storageHandle;
private final boolean databaseBacked;
private final boolean closeRepository;
private final AtomicBoolean closed = new AtomicBoolean();
private final TaxDslParser parser = new TaxDslParser();
private final AstToModelMapper astMapper = new AstToModelMapper();
private final ModelDiffer differ = new ModelDiffer();
/** Open the shared system repository through the reusable storage library. */
public DslGitRepository(HibernateRepositoryFactory storageFactory) {
this(storageFactory, DEFAULT_REPOSITORY_NAME);
}
/** Open a named logical repository through the reusable storage library. */
public DslGitRepository(
HibernateRepositoryFactory storageFactory,
String repositoryName) {
this(openStorage(storageFactory, repositoryName));
}
private DslGitRepository(HibernateGitStorage storageHandle) {
this.gitRepo = storageHandle.repository();
this.storageHandle = storageHandle;
this.databaseBacked = true;
this.closeRepository = false;
log.info("Initialised database-backed DslGitRepository ({})",
gitRepo.getClass().getSimpleName());
}
/** Create an owned, volatile in-memory repository for unit tests. */
public DslGitRepository() {
this(new InMemoryRepository(new DfsRepositoryDescription(DEFAULT_REPOSITORY_NAME)), true);
}
/** Use a caller-provided JGit repository without taking ownership of it. */
public DslGitRepository(Repository gitRepo) {
this(gitRepo, false);
}
DslGitRepository(Repository gitRepo, boolean closeRepository) {
this.gitRepo = Objects.requireNonNull(gitRepo, "gitRepo");
this.storageHandle = null;
this.databaseBacked = false;
this.closeRepository = closeRepository;
log.info("Initialised DslGitRepository ({})", gitRepo.getClass().getSimpleName());
}
private static HibernateGitStorage openStorage(
HibernateRepositoryFactory storageFactory,
String repositoryName) {
Objects.requireNonNull(storageFactory, "storageFactory");
return storageFactory.open(new RepositoryName(repositoryName));
}
// ── Commit operations ───────────────────────────────────────────
/**
* Commit DSL text to a branch and update the branch optimistically.
*
* @return the new commit SHA
* @throws IOException if object insertion or the ref update fails
*/
public String commitDsl(String branch, String dslText, String author, String message)
throws IOException {
PersonIdent actor = personIdent(author);
String commitMessage = message != null ? message : "DSL update";
String refName = Constants.R_HEADS + branch;
Ref branchRef = gitRepo.getRefDatabase().exactRef(refName);
try (ObjectInserter inserter = gitRepo.newObjectInserter()) {
ObjectId blobId = inserter.insert(
Constants.OBJ_BLOB, dslText.getBytes(StandardCharsets.UTF_8));
TreeFormatter tree = new TreeFormatter();
tree.append(DSL_FILENAME, FileMode.REGULAR_FILE, blobId);
ObjectId treeId = inserter.insert(tree);
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
commit.setAuthor(actor);
commit.setCommitter(actor);
commit.setMessage(commitMessage);
if (branchRef != null) {
commit.setParentId(branchRef.getObjectId());
}
ObjectId commitId = inserter.insert(commit);
inserter.flush();
RefUpdate.Result result = updateRef(
refName,
commitId,
branchRef != null ? branchRef.getObjectId() : ObjectId.zeroId(),
false,
actor,
"commit: " + commitMessage);
log.info("Committed DSL to branch '{}': {} (ref update: {})",
branch, commitId.name(), result);
return commitId.name();
}
}
// ── Read operations ─────────────────────────────────────────────
/** Read DSL text at a specific commit. */
public String getDslAtCommit(String commitId) throws IOException {
ObjectId objectId = ObjectId.fromString(commitId);
try (RevWalk walk = new RevWalk(gitRepo)) {
return readDslFromTree(walk.parseCommit(objectId).getTree());
}
}
/** Read DSL text at the HEAD of a branch. */
public String getDslAtHead(String branch) throws IOException {
Ref ref = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
if (ref == null) {
return null;
}
return getDslAtCommit(ref.getObjectId().name());
}
/** Return commit history of a branch, newest first. */
public List<DslCommit> getDslHistory(String branch) throws IOException {
Ref ref = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
if (ref == null) {
return List.of();
}
List<DslCommit> history = new ArrayList<>();
try (RevWalk walk = new RevWalk(gitRepo)) {
walk.markStart(walk.parseCommit(ref.getObjectId()));
for (RevCommit commit : walk) {
PersonIdent authorIdent = commit.getAuthorIdent();
history.add(new DslCommit(
commit.name(),
authorIdent.getName(),
Instant.ofEpochSecond(commit.getCommitTime()),
commit.getFullMessage()));
}
}
return history;
}
// ── Branch operations ───────────────────────────────────────────
/** List all local branches. */
public List<DslBranch> listBranches() throws IOException {
List<DslBranch> branches = new ArrayList<>();
for (Ref ref : gitRepo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS)) {
String name = ref.getName().substring(Constants.R_HEADS.length());
String headCommitId = ref.getObjectId() != null ? ref.getObjectId().name() : null;
Instant created = oldestCommitTime(ref);
branches.add(new DslBranch(name, headCommitId, created));
}
return branches;
}
private Instant oldestCommitTime(Ref ref) throws IOException {
if (ref.getObjectId() == null) {
return null;
}
try (RevWalk walk = new RevWalk(gitRepo)) {
walk.markStart(walk.parseCommit(ref.getObjectId()));
RevCommit oldest = null;
for (RevCommit commit : walk) {
oldest = commit;
}
return oldest != null ? Instant.ofEpochSecond(oldest.getCommitTime()) : null;
}
}
/** Create a new branch at the HEAD of an existing branch. */
public String createBranch(String newBranch, String fromBranch) throws IOException {
Ref source = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + fromBranch);
if (source == null) {
return null;
}
String targetRef = Constants.R_HEADS + newBranch;
updateRef(
targetRef,
source.getObjectId(),
ObjectId.zeroId(),
false,
systemIdent(),
"branch: created from " + fromBranch);
log.info("Created branch '{}' from '{}' at {}",
newBranch, fromBranch, source.getObjectId().name());
return source.getObjectId().name();
}
// ── Diff operations ─────────────────────────────────────────────
/** Compute a semantic model diff between two commits. */
public ModelDiff diffBetween(String fromCommitId, String toCommitId) throws IOException {
String beforeDsl = getDslAtCommit(fromCommitId);
String afterDsl = getDslAtCommit(toCommitId);
return differ.diff(toModel(beforeDsl), toModel(afterDsl));
}
/** Compute a semantic model diff between branch heads. */
public ModelDiff diffBranches(String fromBranch, String toBranch) throws IOException {
return differ.diff(toModel(getDslAtHead(fromBranch)), toModel(getDslAtHead(toBranch)));
}
private CanonicalArchitectureModel toModel(String dsl) {
return dsl != null ? astMapper.map(parser.parse(dsl)) : null;
}
/** Produce a JGit-native unified text diff between two commits. */
public String textDiff(String fromCommitId, String toCommitId) throws IOException {
ObjectId fromObjectId = ObjectId.fromString(fromCommitId);
ObjectId toObjectId = ObjectId.fromString(toCommitId);
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit fromCommit = walk.parseCommit(fromObjectId);
RevCommit toCommit = walk.parseCommit(toObjectId);
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (DiffFormatter formatter = new DiffFormatter(output)) {
formatter.setRepository(gitRepo);
formatter.format(fromCommit.getTree(), toCommit.getTree());
formatter.flush();
}
return output.toString(StandardCharsets.UTF_8);
}
}
/** Return JGit diff metadata between two commits. */
public List<DiffEntry> jgitDiffEntries(String fromCommitId, String toCommitId)
throws IOException {
ObjectId fromObjectId = ObjectId.fromString(fromCommitId);
ObjectId toObjectId = ObjectId.fromString(toCommitId);
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit fromCommit = walk.parseCommit(fromObjectId);
RevCommit toCommit = walk.parseCommit(toObjectId);
try (DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
formatter.setRepository(gitRepo);
return formatter.scan(fromCommit.getTree(), toCommit.getTree());
}
}
}
// ── Cherry-pick and merge operations ────────────────────────────
/** Cherry-pick a commit onto a target branch. */
public String cherryPick(String commitId, String targetBranch) throws IOException {
ObjectId pickObjectId = ObjectId.fromString(commitId);
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit pickCommit = walk.parseCommit(pickObjectId);
String targetRefName = Constants.R_HEADS + targetBranch;
Ref targetRef = gitRepo.getRefDatabase().exactRef(targetRefName);
if (targetRef == null) {
log.warn("Target branch '{}' not found for cherry-pick", targetBranch);
return null;
}
RevCommit targetHead = walk.parseCommit(targetRef.getObjectId());
ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(gitRepo, true);
if (!merger.merge(targetHead, pickCommit)) {
log.warn("Cherry-pick merge conflict for {} onto '{}'", commitId, targetBranch);
return null;
}
PersonIdent committer = systemIdent();
try (ObjectInserter inserter = gitRepo.newObjectInserter()) {
CommitBuilder newCommit = new CommitBuilder();
newCommit.setTreeId(merger.getResultTreeId());
newCommit.setParentId(targetHead);
newCommit.setAuthor(pickCommit.getAuthorIdent());
newCommit.setCommitter(committer);
newCommit.setMessage("Cherry-pick: " + pickCommit.getShortMessage());
ObjectId newCommitId = inserter.insert(newCommit);
inserter.flush();
updateRef(
targetRefName,
newCommitId,
targetHead,
false,
committer,
"cherry-pick: " + pickCommit.getShortMessage());
log.info("Cherry-picked {} onto '{}': {}",
commitId, targetBranch, newCommitId.name());
return newCommitId.name();
}
}
}
/** Merge one branch into another. */
public String merge(String fromBranch, String intoBranch) throws IOException {
String fromRefName = Constants.R_HEADS + fromBranch;
String intoRefName = Constants.R_HEADS + intoBranch;
Ref fromRef = gitRepo.getRefDatabase().exactRef(fromRefName);
Ref intoRef = gitRepo.getRefDatabase().exactRef(intoRefName);
if (fromRef == null || intoRef == null) {
log.warn("Cannot merge: branch not found (from={}, into={})", fromBranch, intoBranch);
return null;
}
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit fromCommit = walk.parseCommit(fromRef.getObjectId());
RevCommit intoCommit = walk.parseCommit(intoRef.getObjectId());
PersonIdent actor = systemIdent();
if (walk.isMergedInto(fromCommit, intoCommit)) {
log.info("Already merged: '{}' is ancestor of '{}'", fromBranch, intoBranch);
return intoCommit.name();
}
if (walk.isMergedInto(intoCommit, fromCommit)) {
updateRef(
intoRefName,
fromCommit,
intoCommit,
false,
actor,
"merge: fast-forward " + fromBranch + " into " + intoBranch);
log.info("Fast-forward merge '{}' into '{}': {}",
fromBranch, intoBranch, fromCommit.name());
return fromCommit.name();
}
ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(gitRepo, true);
if (!merger.merge(intoCommit, fromCommit)) {
log.warn("Merge conflict: '{}' into '{}'", fromBranch, intoBranch);
return null;
}
try (ObjectInserter inserter = gitRepo.newObjectInserter()) {
CommitBuilder mergeCommit = new CommitBuilder();
mergeCommit.setTreeId(merger.getResultTreeId());
mergeCommit.setParentIds(intoCommit, fromCommit);
mergeCommit.setAuthor(actor);
mergeCommit.setCommitter(actor);
mergeCommit.setMessage("Merge branch '" + fromBranch + "' into " + intoBranch);
ObjectId mergeCommitId = inserter.insert(mergeCommit);
inserter.flush();
updateRef(
intoRefName,
mergeCommitId,
intoCommit,
false,
actor,
"merge: " + fromBranch + " into " + intoBranch);
log.info("Merged '{}' into '{}': {}",
fromBranch, intoBranch, mergeCommitId.name());
return mergeCommitId.name();
}
}
}
// ── Revert and undo operations ──────────────────────────────────
/** Revert a specific commit on a branch by creating a new commit. */
public String revert(String commitId, String branch) throws IOException {
ObjectId revertObjectId = ObjectId.fromString(commitId);
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit revertCommit = walk.parseCommit(revertObjectId);
if (revertCommit.getParentCount() == 0) {
log.warn("Cannot revert initial commit {}", commitId);
return null;
}
String refName = Constants.R_HEADS + branch;
Ref branchRef = gitRepo.getRefDatabase().exactRef(refName);
if (branchRef == null) {
log.warn("Branch '{}' not found for revert", branch);
return null;
}
RevCommit branchHead = walk.parseCommit(branchRef.getObjectId());
RevCommit parentCommit = walk.parseCommit(revertCommit.getParent(0));
ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(gitRepo, true);
merger.setBase(revertCommit);
if (!merger.merge(branchHead, parentCommit)) {
log.warn("Revert conflict for {} on '{}'", commitId, branch);
return null;
}
PersonIdent actor = systemIdent();
try (ObjectInserter inserter = gitRepo.newObjectInserter()) {
CommitBuilder newCommit = new CommitBuilder();
newCommit.setTreeId(merger.getResultTreeId());
newCommit.setParentId(branchHead);
newCommit.setAuthor(actor);
newCommit.setCommitter(actor);
newCommit.setMessage("Revert: " + revertCommit.getShortMessage());
ObjectId newCommitId = inserter.insert(newCommit);
inserter.flush();
updateRef(
refName,
newCommitId,
branchHead,
false,
actor,
"revert: " + revertCommit.getShortMessage());
log.info("Reverted {} on '{}': {}", commitId, branch, newCommitId.name());
return newCommitId.name();
}
}
}
/** Move a branch back to its first parent. */
public String undoLast(String branch) throws IOException {
String refName = Constants.R_HEADS + branch;
Ref branchRef = gitRepo.getRefDatabase().exactRef(refName);
if (branchRef == null) {
log.warn("Branch '{}' not found for undo", branch);
return null;
}
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit headCommit = walk.parseCommit(branchRef.getObjectId());
if (headCommit.getParentCount() == 0) {
log.warn("Cannot undo: branch '{}' has only the initial commit", branch);
return null;
}
RevCommit parentCommit = walk.parseCommit(headCommit.getParent(0));
updateRef(
refName,
parentCommit,
headCommit,
true,
systemIdent(),
"reset: moving to " + parentCommit.name());
log.info("Undo last on '{}': {} -> {}",
branch, headCommit.name(), parentCommit.name());
return parentCommit.name();
}
}
/** Restore DSL content from a historic commit as a new branch commit. */
public String restore(String commitId, String branch) throws IOException {
String dslText;
try {
dslText = getDslAtCommit(commitId);
} catch (org.eclipse.jgit.errors.MissingObjectException exception) {
log.warn("Cannot restore: commit {} not found", commitId);
return null;
}
if (dslText == null) {
log.warn("Cannot restore: commit {} has no DSL content", commitId);
return null;
}
return commitDsl(
branch,
dslText,
"taxonomy",
"Restored from version " + commitId.substring(0, 7));
}
// ── State query helpers ─────────────────────────────────────────
/** Return the HEAD SHA for a branch, or {@code null}. */
public String getHeadCommit(String branch) throws IOException {
Ref ref = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
return ref != null && ref.getObjectId() != null ? ref.getObjectId().name() : null;
}
/** Return all branch names without the {@code refs/heads/} prefix. */
public List<String> getBranchNames() throws IOException {
List<String> names = new ArrayList<>();
for (Ref ref : gitRepo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS)) {
names.add(ref.getName().substring(Constants.R_HEADS.length()));
}
return names;
}
/** Count commits reachable from a branch. */
public int getCommitCount(String branch) throws IOException {
Ref ref = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
if (ref == null) {
return 0;
}
int count = 0;
try (RevWalk walk = new RevWalk(gitRepo)) {
walk.markStart(walk.parseCommit(ref.getObjectId()));
for (RevCommit ignored : walk) {
count++;
}
}
return count;
}
/** Return {@code [ahead, behind]} commit counts for two branches. */
public int[] getAheadBehindCounts(String branch, String baseBranch) throws IOException {
Ref branchRef = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
Ref baseRef = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + baseBranch);
if (branchRef == null || baseRef == null) {
return new int[]{0, 0};
}
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit branchHead = walk.parseCommit(branchRef.getObjectId());
RevCommit baseHead = walk.parseCommit(baseRef.getObjectId());
if (branchHead.equals(baseHead)) {
return new int[]{0, 0};
}
int ahead = countCommitsNotIn(walk, branchHead, baseHead);
int behind = countCommitsNotIn(walk, baseHead, branchHead);
return new int[]{ahead, behind};
}
}
private static int countCommitsNotIn(
RevWalk walk,
RevCommit tip,
RevCommit exclude) throws IOException {
walk.reset();
walk.markStart(tip);
walk.markUninteresting(exclude);
int count = 0;
for (RevCommit ignored : walk) {
count++;
}
return count;
}
/** Return metadata for the HEAD commit of a branch. */
public DslCommit getHeadCommitInfo(String branch) throws IOException {
Ref ref = gitRepo.getRefDatabase().exactRef(Constants.R_HEADS + branch);
if (ref == null) {
return null;
}
try (RevWalk walk = new RevWalk(gitRepo)) {
RevCommit commit = walk.parseCommit(ref.getObjectId());
PersonIdent authorIdent = commit.getAuthorIdent();
return new DslCommit(
commit.name(),
authorIdent.getName(),
Instant.ofEpochSecond(commit.getCommitTime()),
commit.getFullMessage());
}
}
private String readDslFromTree(RevTree tree) throws IOException {
try (TreeWalk treeWalk = TreeWalk.forPath(gitRepo, DSL_FILENAME, tree)) {
if (treeWalk == null) {
return null;
}
ObjectLoader loader = gitRepo.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB);
return new String(loader.getBytes(), StandardCharsets.UTF_8);
}
}
private RefUpdate.Result updateRef(
String refName,
ObjectId newObjectId,
ObjectId expectedOldObjectId,
boolean force,
PersonIdent actor,
String reflogMessage) throws IOException {
RefUpdate update = gitRepo.updateRef(refName);
update.setNewObjectId(newObjectId);
update.setExpectedOldObjectId(expectedOldObjectId);
update.setForceUpdate(force);
update.setRefLogIdent(actor);
update.setRefLogMessage(reflogMessage, true);
RefUpdate.Result result = update.update();
requireSuccessfulUpdate(refName, result);
return result;
}
private static void requireSuccessfulUpdate(String refName, RefUpdate.Result result)
throws IOException {
if (result != RefUpdate.Result.NEW
&& result != RefUpdate.Result.FAST_FORWARD
&& result != RefUpdate.Result.FORCED
&& result != RefUpdate.Result.NO_CHANGE) {
throw new IOException("Ref update failed for " + refName + ": " + result);
}
}
private static PersonIdent personIdent(String author) {
String name = author != null ? author : "taxonomy";
String email = author != null ? author : "taxonomy@system";
return new PersonIdent(name, email);
}
private static PersonIdent systemIdent() {
return new PersonIdent("taxonomy", "taxonomy@system");
}
/** Expose the underlying public JGit repository for advanced operations. */
public Repository getGitRepository() {
return gitRepo;
}
/** Return whether this instance uses persistent Hibernate-backed storage. */
public boolean isDatabaseBacked() {
return databaseBacked;
}
/** Delete a non-protected branch and record a queryable reflog entry. */
public boolean deleteBranch(String branchName) throws IOException {
if ("draft".equals(branchName)
|| "accepted".equals(branchName)
|| "main".equals(branchName)) {
throw new IllegalArgumentException("Cannot delete protected branch: " + branchName);
}
String refName = Constants.R_HEADS + branchName;
Ref ref = gitRepo.getRefDatabase().exactRef(refName);
if (ref == null) {
return false;
}
RefUpdate update = gitRepo.updateRef(refName);
update.setExpectedOldObjectId(ref.getObjectId());
update.setForceUpdate(true);
update.setRefLogIdent(systemIdent());
update.setRefLogMessage("branch: deleted " + branchName, true);
RefUpdate.Result result = update.delete();
if (result != RefUpdate.Result.FORCED
&& result != RefUpdate.Result.FAST_FORWARD) {
throw new IOException("Ref deletion failed for " + refName + ": " + result);
}
log.info("Deleted branch '{}' (ref update: {})", branchName, result);
return true;
}
/** Close the library handle or an owned in-memory repository exactly once. */
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
if (storageHandle != null) {
storageHandle.close();
} else if (closeRepository) {
gitRepo.close();
}
}
}