JGitRepositoryVersionedWorkflowStore.java
package org.hammer.audio.infrastructure.workflow.store;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
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.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.RawParseUtils;
import org.hammer.audio.workflow.store.CommitId;
import org.hammer.audio.workflow.store.CommitInfo;
import org.hammer.audio.workflow.store.CommitMetadata;
import org.hammer.audio.workflow.store.RefUpdateResult;
import org.hammer.audio.workflow.store.StaleWorkflowHeadException;
import org.hammer.audio.workflow.store.VersionedWorkflowStore;
import org.hammer.audio.workflow.store.WorkflowSnapshot;
/**
* Repository-based {@link VersionedWorkflowStore} implementation shared by storage adapters.
*
* <p>The repository lifecycle belongs to the surrounding adapter. This class deliberately uses only
* public JGit APIs and never opens a filesystem repository itself.
*/
final class JGitRepositoryVersionedWorkflowStore implements VersionedWorkflowStore {
private static final String WORKFLOW_DSL_PATH = "workflow.dsl";
private static final String WORKFLOW_ID_PATH = "workflow.id";
private static final ObjectId ZERO_ID = ObjectId.zeroId();
private final Repository repository;
JGitRepositoryVersionedWorkflowStore(Repository repository) {
this.repository = Objects.requireNonNull(repository, "repository");
}
@Override
public CommitId commit(String branch, WorkflowSnapshot snapshot, CommitMetadata metadata) {
return commit(branch, null, snapshot, metadata, false);
}
@Override
public CommitId commitIfHead(
String branch, CommitId expectedHead, WorkflowSnapshot snapshot, CommitMetadata metadata) {
return commit(branch, expectedHead, snapshot, metadata, true);
}
private CommitId commit(
String branch,
CommitId expectedHead,
WorkflowSnapshot snapshot,
CommitMetadata metadata,
boolean conditional) {
if (branch == null || branch.isBlank()) {
throw new IllegalArgumentException("branch must not be blank");
}
Objects.requireNonNull(snapshot, "snapshot");
Objects.requireNonNull(metadata, "metadata");
String refName = toRefName(branch);
try (ObjectInserter inserter = repository.newObjectInserter()) {
ObjectId parent = repository.resolve(refName);
if (conditional) {
assertExpectedHead(branch, expectedHead, parent);
}
ObjectId blobDsl =
inserter.insert(Constants.OBJ_BLOB, snapshot.dslText().getBytes(StandardCharsets.UTF_8));
ObjectId blobWorkflowId =
inserter.insert(
Constants.OBJ_BLOB, snapshot.workflowId().getBytes(StandardCharsets.UTF_8));
org.eclipse.jgit.lib.TreeFormatter formatter = new org.eclipse.jgit.lib.TreeFormatter();
formatter.append(WORKFLOW_DSL_PATH, FileMode.REGULAR_FILE, blobDsl);
formatter.append(WORKFLOW_ID_PATH, FileMode.REGULAR_FILE, blobWorkflowId);
ObjectId treeId = inserter.insert(formatter);
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
if (parent != null) {
commit.setParentIds(parent);
}
PersonIdent ident = toPersonIdent(metadata);
commit.setAuthor(ident);
commit.setCommitter(ident);
commit.setMessage(metadata.message());
ObjectId commitId = inserter.insert(commit);
inserter.flush();
RefUpdate update = repository.updateRef(refName);
update.setExpectedOldObjectId(parent == null ? ZERO_ID : parent);
update.setNewObjectId(commitId);
update.setRefLogMessage("workflow checkpoint", false);
RefUpdate.Result result = update.update();
if (!isSuccessfulRefUpdate(result)) {
if (conditional) {
throw new StaleWorkflowHeadException(
branch, expectedHead, toCommitId(repository.resolve(refName)));
}
throw new IllegalStateException("Failed to update ref '" + refName + "': " + result);
}
return new CommitId(commitId.name());
} catch (IOException ex) {
throw new IllegalStateException("Failed to commit workflow snapshot", ex);
}
}
@Override
public WorkflowSnapshot loadAtCommit(CommitId commitId) {
Objects.requireNonNull(commitId, "commitId");
ObjectId objectId = parseObjectId(commitId);
try (RevWalk walk = new RevWalk(repository)) {
RevCommit commit = walk.parseCommit(objectId);
return readSnapshot(commit);
} catch (IOException ex) {
NoSuchElementException exception =
new NoSuchElementException("Commit not found: " + commitId);
exception.initCause(ex);
throw exception;
}
}
@Override
public WorkflowSnapshot loadHead(String branch) {
if (branch == null || branch.isBlank()) {
throw new IllegalArgumentException("branch must not be blank");
}
String refName = toRefName(branch);
try {
ObjectId head = repository.resolve(refName);
if (head == null) {
throw new NoSuchElementException("Branch not found: " + branch);
}
return loadAtCommit(new CommitId(head.name()));
} catch (IOException ex) {
throw new IllegalStateException("Failed to resolve branch: " + branch, ex);
}
}
@Override
public RefUpdateResult updateRef(String refName, CommitId expectedOldCommit, CommitId newCommit) {
if (refName == null || refName.isBlank()) {
throw new IllegalArgumentException("refName must not be blank");
}
Objects.requireNonNull(newCommit, "newCommit");
String normalizedRef = toRefName(refName);
ObjectId newObjectId = parseObjectId(newCommit);
verifyCommitExists(newObjectId, newCommit);
try {
RefUpdate update = repository.updateRef(normalizedRef);
update.setExpectedOldObjectId(
expectedOldCommit == null ? ZERO_ID : parseObjectId(expectedOldCommit));
update.setNewObjectId(newObjectId);
update.setForceUpdate(true);
update.setRefLogMessage("workflow ref update", false);
RefUpdate.Result result = update.update();
return mapRefUpdateResult(result);
} catch (IOException ex) {
throw new IllegalStateException("Failed to update ref: " + refName, ex);
}
}
private void verifyCommitExists(ObjectId objectId, CommitId commitId) {
try (RevWalk walk = new RevWalk(repository)) {
walk.parseCommit(objectId);
} catch (IOException ex) {
NoSuchElementException exception =
new NoSuchElementException("Commit not found: " + commitId.value());
exception.initCause(ex);
throw exception;
}
}
@Override
public List<CommitInfo> history(String refName, int limit) {
if (refName == null || refName.isBlank()) {
throw new IllegalArgumentException("refName must not be blank");
}
if (limit < 0) {
throw new IllegalArgumentException("limit must be >= 0");
}
if (limit == 0) {
return List.of();
}
String normalizedRef = toRefName(refName);
try {
ObjectId head = repository.resolve(normalizedRef);
if (head == null) {
return List.of();
}
List<CommitInfo> history = new ArrayList<>();
try (RevWalk walk = new RevWalk(repository)) {
walk.markStart(walk.parseCommit(head));
for (RevCommit commit : walk) {
history.add(
new CommitInfo(
new CommitId(commit.getId().name()),
commitMetadataFrom(commit),
readWorkflowId(commit)));
if (history.size() >= limit) {
break;
}
}
}
return List.copyOf(history);
} catch (IOException ex) {
throw new IllegalStateException("Failed to load history for ref: " + refName, ex);
}
}
private static void assertExpectedHead(
String branch, CommitId expectedHead, ObjectId actualHead) {
ObjectId expectedObjectId = expectedHead == null ? null : parseObjectId(expectedHead);
if (!Objects.equals(expectedObjectId, actualHead)) {
throw new StaleWorkflowHeadException(branch, expectedHead, toCommitId(actualHead));
}
}
private static CommitId toCommitId(ObjectId objectId) {
return objectId == null ? null : new CommitId(objectId.name());
}
private static String toRefName(String branchOrRef) {
if (branchOrRef.startsWith("refs/")) {
return branchOrRef;
}
return "refs/heads/" + branchOrRef;
}
private static PersonIdent toPersonIdent(CommitMetadata metadata) {
String sanitizedAuthor = metadata.author().trim().replaceAll("[\\r\\n]", " ");
String emailLocalPart = sanitizedAuthor.replaceAll("[^A-Za-z0-9._-]", "-");
if (emailLocalPart.isBlank()) {
emailLocalPart = "workflow-author";
}
Instant instant = metadata.timestamp();
return new PersonIdent(
sanitizedAuthor, emailLocalPart + "@audio-analyzer.invalid", instant, ZoneOffset.UTC);
}
private static boolean isSuccessfulRefUpdate(RefUpdate.Result result) {
return result == RefUpdate.Result.NEW
|| result == RefUpdate.Result.FAST_FORWARD
|| result == RefUpdate.Result.FORCED
|| result == RefUpdate.Result.NO_CHANGE;
}
private static RefUpdateResult mapRefUpdateResult(RefUpdate.Result result) {
if (isSuccessfulRefUpdate(result)) {
return RefUpdateResult.SUCCESS;
}
return RefUpdateResult.STALE;
}
private static ObjectId parseObjectId(CommitId commitId) {
try {
return ObjectId.fromString(commitId.value());
} catch (IllegalArgumentException ex) {
NoSuchElementException exception =
new NoSuchElementException("Invalid commit id: " + commitId.value());
exception.initCause(ex);
throw exception;
}
}
private WorkflowSnapshot readSnapshot(RevCommit commit) throws IOException {
String workflowId = readFile(commit, WORKFLOW_ID_PATH);
String dslText = readFile(commit, WORKFLOW_DSL_PATH);
return new WorkflowSnapshot(workflowId, dslText);
}
private String readWorkflowId(RevCommit commit) throws IOException {
return readFile(commit, WORKFLOW_ID_PATH);
}
private String readFile(RevCommit commit, String filePath) throws IOException {
try (TreeWalk walk = TreeWalk.forPath(repository, filePath, commit.getTree())) {
if (walk == null) {
throw new NoSuchElementException(
"File '" + filePath + "' not found in commit " + commit.getId().name());
}
return RawParseUtils.decode(repository.open(walk.getObjectId(0)).getBytes());
}
}
private static CommitMetadata commitMetadataFrom(RevCommit commit) {
PersonIdent author = commit.getAuthorIdent();
String authorName = author == null ? "unknown" : author.getName();
Instant timestamp = author == null ? Instant.EPOCH : author.getWhenAsInstant();
return new CommitMetadata(authorName, commit.getFullMessage().trim(), timestamp);
}
}