GitNativeSyncIntegrationService.java
package com.taxonomy.workspace.service;
import com.taxonomy.dsl.merge.TaxDslMergeResult;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.versioning.service.SemanticGitMergeService;
import com.taxonomy.workspace.model.SyncState;
import com.taxonomy.workspace.model.SystemRepository;
import com.taxonomy.workspace.model.UserWorkspace;
import com.taxonomy.workspace.repository.SyncStateRepository;
import com.taxonomy.workspace.repository.UserWorkspaceRepository;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.Instant;
/**
* Portfolio-aware replacement for the legacy copy-based synchronization path.
*
* <p>Before pull or push, durable project decisions are projected into the
* branch DSL through a workspace-owned port. Isolated repositories use a
* tracked three-way semantic base instead of replacing the complete
* architecture file. Every synchronization path carries the exact logical
* repository, workspace/central scope and branch into both Git and portfolio
* materialisation.</p>
*/
@Service
@Primary
public class GitNativeSyncIntegrationService extends SyncIntegrationService {
private static final String WORKSPACE_BRANCH = "main";
private static final String SEEDED_BRANCH = "draft";
private static final String TRACKING_BRANCH = "sync-base";
private final SyncStateRepository syncStateRepository;
private final UserWorkspaceRepository workspaceRepository;
private final SystemRepositoryService systemRepositoryService;
private final DslGitRepositoryFactory repositoryFactory;
private final SemanticGitMergeService semanticMergeService;
private final WorkspacePortfolioGitPort portfolioGitPort;
private final WorkspaceContextResolver contextResolver;
private final WorkspaceArchitectureVersionPort editorVersions;
public GitNativeSyncIntegrationService(SyncStateRepository syncStateRepository,
UserWorkspaceRepository workspaceRepository,
SystemRepositoryService systemRepositoryService,
DslGitRepositoryFactory repositoryFactory,
SemanticGitMergeService semanticMergeService,
WorkspacePortfolioGitPort portfolioGitPort,
WorkspaceContextResolver contextResolver, WorkspaceArchitectureVersionPort editorVersions) {
super(syncStateRepository, workspaceRepository, systemRepositoryService, repositoryFactory);
this.syncStateRepository = syncStateRepository;
this.workspaceRepository = workspaceRepository;
this.systemRepositoryService = systemRepositoryService;
this.repositoryFactory = repositoryFactory;
this.semanticMergeService = semanticMergeService;
this.portfolioGitPort = portfolioGitPort;
this.contextResolver = contextResolver;
this.editorVersions = editorVersions;
}
@Override
public String syncFromShared(String username, String userBranch) throws IOException {
WorkspaceContext context = resolveWorkspaceContext(username, userBranch);
if (context.workspaceId() == null) {
return mergeWithinRepository(
username, context, context.currentBranch(), true);
}
return pullAcrossRepositories(username, context, context.currentBranch());
}
@Override
public String publishToShared(String username, String userBranch) throws IOException {
WorkspaceContext context = resolveWorkspaceContext(username, userBranch);
if (context.workspaceId() == null) {
return mergeWithinRepository(
username, context, context.currentBranch(), false);
}
return publishAcrossRepositories(username, context, context.currentBranch());
}
@Override
public String syncFromSharedToWorkspace(String username, String workspaceId)
throws IOException {
WorkspaceContext context = resolveExplicitWorkspaceContext(
username, workspaceId, WORKSPACE_BRANCH);
return pullAcrossRepositories(username, context, WORKSPACE_BRANCH);
}
@Override
public String publishFromWorkspaceToShared(String username, String workspaceId)
throws IOException {
WorkspaceContext context = resolveExplicitWorkspaceContext(
username, workspaceId, WORKSPACE_BRANCH);
return publishAcrossRepositories(username, context, WORKSPACE_BRANCH);
}
@Override
public String resolveDiverged(String username,
String userBranch,
DivergedStrategy strategy) throws IOException {
if (strategy == DivergedStrategy.MERGE) {
String commit = syncFromShared(username, userBranch);
return "Semantically merged source into your branch: " + abbreviate(commit);
}
return super.resolveDiverged(username, userBranch, strategy);
}
private String pullAcrossRepositories(String username,
WorkspaceContext context,
String userBranch) throws IOException {
RepositoryContext selected = RepositoryContext.workspace(context.repositoryId(), context.workspaceId(), userBranch, username);
return editorVersions.version(selected, "Integrate architecture versions from source", () -> pullAcrossRepositoriesVersion(username, context, userBranch));
}
private String pullAcrossRepositoriesVersion(String username,
WorkspaceContext context,
String userBranch) throws IOException {
UserWorkspace workspaceMetadata = requireWorkspace(context.workspaceId());
SystemRepository sourceMetadata = requireSourceRepository(workspaceMetadata);
requireMatchingRepository(context, sourceMetadata);
String sourceBranch = sourceBranch(workspaceMetadata, sourceMetadata);
DslGitRepository sourceRepository =
repositoryFactory.getCentralRepository(sourceMetadata.getRepositoryId());
DslGitRepository workspaceGit =
repositoryFactory.openWorkspaceRepository(context.workspaceId());
WorkspaceMergeState state = initialiseWorkspaceMergeBase(
workspaceGit, sourceRepository, sourceBranch, username, userBranch);
portfolioGitPort.commitPortfolio(userBranch,
"Project requirements before pull", username, context);
String ours = requiredDsl(workspaceGit, userBranch,
"Workspace branch has no content: " + userBranch);
String theirs = requiredDsl(sourceRepository, sourceBranch,
"Source branch has no content: " + sourceBranch);
TaxDslMergeResult merge = semanticMergeService.mergeContent(
state.baseDsl(), ours, theirs);
requireSuccess("Pull from source", merge);
String localCommit = commitIfChanged(
workspaceGit,
userBranch,
merge.mergedText(),
ours,
username,
"Semantic pull from " + sourceMetadata.getSlug());
trackBase(workspaceGit, merge.mergedText(), username);
portfolioGitPort.materializePortfolio(merge.mergedText(), username, context);
String sourceHead = sourceRepository.getHeadCommit(sourceBranch);
workspaceMetadata.setLastFetchedCommit(sourceHead);
workspaceMetadata.setLastIntegratedCommit(sourceHead);
workspaceMetadata.setCurrentCommit(localCommit);
workspaceMetadata.setLastAccessedAt(Instant.now());
workspaceRepository.save(workspaceMetadata);
updateAfterSync(username, sourceHead != null ? sourceHead : localCommit);
return localCommit;
}
private String publishAcrossRepositories(String username,
WorkspaceContext context,
String userBranch) throws IOException {
RepositoryContext selected = RepositoryContext.workspace(context.repositoryId(), context.workspaceId(), userBranch, username);
return editorVersions.version(selected, "Publish architecture workspace", () -> publishAcrossRepositoriesVersion(username, context, userBranch));
}
private String publishAcrossRepositoriesVersion(String username,
WorkspaceContext context,
String userBranch) throws IOException {
UserWorkspace workspaceMetadata = requireWorkspace(context.workspaceId());
SystemRepository sourceMetadata = requireSourceRepository(workspaceMetadata);
requireMatchingRepository(context, sourceMetadata);
String sourceBranch = sourceBranch(workspaceMetadata, sourceMetadata);
DslGitRepository sourceRepository =
repositoryFactory.getCentralRepository(sourceMetadata.getRepositoryId());
DslGitRepository workspaceGit =
repositoryFactory.openWorkspaceRepository(context.workspaceId());
WorkspaceMergeState state = initialiseWorkspaceMergeBase(
workspaceGit, sourceRepository, sourceBranch, username, userBranch);
portfolioGitPort.commitPortfolio(userBranch,
"Project requirements before push", username, context);
String sourceDsl = valueOrEmpty(sourceRepository.getDslAtHead(sourceBranch));
String workspaceDsl = requiredDsl(workspaceGit, userBranch,
"Workspace branch has no content: " + userBranch);
TaxDslMergeResult merge = semanticMergeService.mergeContent(
state.baseDsl(), sourceDsl, workspaceDsl);
requireSuccess("Publish to source", merge);
String sourceCommit = commitIfChanged(
sourceRepository,
sourceBranch,
merge.mergedText(),
sourceDsl,
username,
"Semantic publish from workspace " + context.workspaceId());
String workspaceCommit = commitIfChanged(
workspaceGit,
userBranch,
merge.mergedText(),
workspaceDsl,
username,
"Integrate source changes after publish");
trackBase(workspaceGit, merge.mergedText(), username);
WorkspaceContext centralContext = new WorkspaceContext(
"shared",
null,
sourceBranch,
requireRepositoryId(sourceMetadata));
portfolioGitPort.materializePortfolio(
merge.mergedText(), "shared", centralContext);
portfolioGitPort.materializePortfolio(
merge.mergedText(), username, context);
workspaceMetadata.setLastFetchedCommit(sourceCommit);
workspaceMetadata.setLastIntegratedCommit(sourceCommit);
workspaceMetadata.setCurrentCommit(workspaceCommit);
workspaceMetadata.setLastAccessedAt(Instant.now());
workspaceRepository.save(workspaceMetadata);
sourceMetadata.setLastPushAt(Instant.now());
systemRepositoryService.save(sourceMetadata);
updateAfterPublish(username, sourceCommit);
return sourceCommit;
}
private String mergeWithinRepository(String username,
WorkspaceContext context,
String userBranch,
boolean pull) throws IOException {
if (context.workspaceId() != null) {
throw new IllegalArgumentException(
"Central synchronization must not carry a workspaceId");
}
DslGitRepository repository =
repositoryFactory.getCentralRepository(context.repositoryId());
portfolioGitPort.commitPortfolio(userBranch,
pull ? "Project requirements before sync from shared"
: "Project requirements before publish",
username,
context);
String from = pull ? sharedBranch() : userBranch;
String into = pull ? userBranch : sharedBranch();
SemanticGitMergeService.MergeOutcome outcome = semanticMergeService.mergeBranches(
repository, from, into, username);
requireSuccess(pull ? "Sync" : "Publish", outcome);
portfolioGitPort.materializePortfolioHead(into, username, context);
if (pull) {
updateAfterSync(username, repository.getHeadCommit(sharedBranch()));
} else {
updateAfterPublish(username, outcome.commitId());
}
return outcome.commitId();
}
/** Establish the common ancestor before any local portfolio projection is committed. */
private WorkspaceMergeState initialiseWorkspaceMergeBase(
DslGitRepository workspaceGit,
DslGitRepository sourceRepository,
String sourceBranch,
String username,
String userBranch) throws IOException {
String existingBase = valueOrEmpty(
workspaceGit.getDslAtHead(TRACKING_BRANCH));
String existingUserBranch = valueOrEmpty(
workspaceGit.getDslAtHead(userBranch));
if (!existingBase.isBlank() && !existingUserBranch.isBlank()) {
return new WorkspaceMergeState(existingBase);
}
String source = valueOrEmpty(sourceRepository.getDslAtHead(sourceBranch));
String seeded = valueOrEmpty(workspaceGit.getDslAtHead(SEEDED_BRANCH));
String commonBase = !seeded.isBlank() ? seeded : source;
if (commonBase.isBlank()) {
throw new IOException("Neither source nor workspace seed contains architecture DSL");
}
if (existingUserBranch.isBlank()) {
workspaceGit.commitDsl(
userBranch,
commonBase,
username,
"Initialize workspace branch from source architecture");
}
if (existingBase.isBlank()) {
workspaceGit.commitDsl(
TRACKING_BRANCH,
commonBase,
username,
"Initialize semantic synchronization base");
}
return new WorkspaceMergeState(commonBase);
}
/** Resolve the exact persistent repository and active branch for one user. */
private WorkspaceContext resolveWorkspaceContext(String username, String requestedBranch) {
RepositoryContext persistent =
contextResolver.resolveRepositoryContextForUser(username);
if (persistent.workspaceId() == null) {
return new WorkspaceContext(
persistent.username(),
null,
persistent.branch(),
persistent.repositoryId());
}
String branch = requestedBranch;
if (branch == null || branch.isBlank()
|| (SEEDED_BRANCH.equals(branch)
&& !SEEDED_BRANCH.equals(persistent.branch()))) {
branch = persistent.branch();
}
if (branch == null || branch.isBlank()) {
branch = WORKSPACE_BRANCH;
}
return new WorkspaceContext(
persistent.username(),
persistent.workspaceId(),
branch,
persistent.repositoryId());
}
/** Resolve an explicitly addressed workspace without inventing repository provenance. */
private WorkspaceContext resolveExplicitWorkspaceContext(
String username, String workspaceId, String branch) {
UserWorkspace workspace = requireWorkspace(workspaceId);
SystemRepository source = requireSourceRepository(workspace);
return new WorkspaceContext(
username,
workspace.getWorkspaceId(),
branch,
requireRepositoryId(source));
}
private UserWorkspace requireWorkspace(String workspaceId) {
if (workspaceId == null || workspaceId.isBlank()) {
throw new IllegalArgumentException("workspaceId must not be blank");
}
return workspaceRepository.findByWorkspaceId(workspaceId.strip())
.orElseThrow(() -> new IllegalStateException(
"Workspace metadata not found: " + workspaceId));
}
private SystemRepository requireSourceRepository(UserWorkspace workspace) {
String repositoryId = workspace.getSourceRepositoryId();
if (repositoryId == null || repositoryId.isBlank()) {
throw new IllegalStateException(
"Workspace has no sourceRepositoryId: " + workspace.getWorkspaceId());
}
return systemRepositoryService.getRepository(repositoryId.strip());
}
private static void requireMatchingRepository(
WorkspaceContext context, SystemRepository sourceRepository) {
String sourceRepositoryId = requireRepositoryId(sourceRepository);
String requestRepositoryId = context.repositoryId();
if (!sourceRepositoryId.equals(requestRepositoryId)) {
throw new IllegalStateException(
"Workspace repository context does not match persisted source provenance: "
+ "request repositoryId=" + requestRepositoryId
+ ", persisted sourceRepositoryId=" + sourceRepositoryId);
}
}
private static String requireRepositoryId(SystemRepository repository) {
if (repository == null || repository.getRepositoryId() == null
|| repository.getRepositoryId().isBlank()) {
throw new IllegalStateException("Source repository has no repositoryId");
}
return repository.getRepositoryId().strip();
}
private static String sourceBranch(
UserWorkspace workspace, SystemRepository sourceRepository) {
if (workspace.getSyncTargetBranch() != null
&& !workspace.getSyncTargetBranch().isBlank()) {
return workspace.getSyncTargetBranch();
}
if (workspace.getSourceBranch() != null && !workspace.getSourceBranch().isBlank()) {
return workspace.getSourceBranch();
}
return sourceRepository.getDefaultBranch();
}
private String sharedBranch() {
return systemRepositoryService.getSharedBranch();
}
private String commitIfChanged(DslGitRepository repository,
String branch,
String merged,
String previous,
String username,
String message) throws IOException {
if (valueOrEmpty(previous).strip().equals(valueOrEmpty(merged).strip())) {
String head = repository.getHeadCommit(branch);
if (head != null) {
return head;
}
}
return repository.commitDsl(branch, merged, username, message);
}
private void trackBase(DslGitRepository workspaceRepository,
String mergedDsl,
String username) throws IOException {
String current = valueOrEmpty(workspaceRepository.getDslAtHead(TRACKING_BRANCH));
if (!current.strip().equals(valueOrEmpty(mergedDsl).strip())) {
workspaceRepository.commitDsl(
TRACKING_BRANCH,
mergedDsl,
username,
"Update semantic synchronization base");
}
}
private void updateAfterSync(String username, String commit) {
SyncState state = getSyncState(username);
state.setLastSyncedCommitId(commit);
state.setLastSyncTimestamp(Instant.now());
state.setSyncStatus("UP_TO_DATE");
state.setUpdatedAt(Instant.now());
syncStateRepository.save(state);
}
private void updateAfterPublish(String username, String commit) {
SyncState state = getSyncState(username);
state.setLastPublishedCommitId(commit);
state.setLastSyncedCommitId(commit);
state.setLastPublishTimestamp(Instant.now());
state.setLastSyncTimestamp(Instant.now());
state.setSyncStatus("UP_TO_DATE");
state.setUnpublishedCommitCount(0);
state.setUpdatedAt(Instant.now());
syncStateRepository.save(state);
}
private static String requiredDsl(DslGitRepository repository,
String branch,
String error) throws IOException {
String dsl = valueOrEmpty(repository.getDslAtHead(branch));
if (dsl.isBlank()) {
throw new IOException(error);
}
return dsl;
}
private static void requireSuccess(String operation,
SemanticGitMergeService.MergeOutcome outcome)
throws IOException {
if (!outcome.success()) {
throw new IOException(operation + " has semantic conflicts: "
+ String.join(", ", outcome.conflicts()));
}
}
private static void requireSuccess(String operation,
TaxDslMergeResult result) throws IOException {
if (!result.isSuccessful()) {
throw new IOException(operation + " has semantic conflicts: "
+ String.join(", ", result.conflictIdentifiers()));
}
}
private static String valueOrEmpty(String value) {
return value == null ? "" : value;
}
private static String abbreviate(String commit) {
return commit == null || commit.length() <= 8 ? commit : commit.substring(0, 8);
}
private record WorkspaceMergeState(String baseDsl) {
}
}