GitRepositoryBootstrap.java
package com.taxonomy.versioning.service;
import com.taxonomy.dsl.export.TaxDslExportService;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.shared.service.AppInitializationStateService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Initialises the DSL Git repository with a {@code draft} branch on application startup.
*
* <p>When the application starts for the first time (empty database), the Git repository
* has no branches and no commits. This causes the frontend to display
* "Git status unavailable" and the version history to show errors, because
* {@code /api/git/state?branch=draft} returns null values.
*
* <p>This component creates an initial commit on the {@code draft} branch containing
* the DSL export of the current taxonomy, so that Git-based features (status bar,
* version history, variants, compare) work correctly from the start.
*
* <p>In synchronous init mode (default), the taxonomy is already loaded by the time
* {@link ApplicationReadyEvent} fires. In asynchronous init mode, this component
* checks {@link AppInitializationStateService#isReady()} and skips if not ready —
* the first user-initiated DSL commit will create the branch in that case.
*
* <p>Controlled by the {@code taxonomy.git.bootstrap} property (default: {@code true}).
* A JVM-wide static guard ensures that only the <em>first</em> Spring context in a
* JVM performs the bootstrap, preventing stale
* {@link org.eclipse.jgit.internal.storage.dfs.DfsBlockCache} entries when
* multiple {@code @SpringBootTest} contexts share the same JVM and
* {@code ddl-auto=create} recreates tables between contexts.
*/
@Component
@ConditionalOnProperty(name = "taxonomy.git.bootstrap", havingValue = "true", matchIfMissing = true)
public class GitRepositoryBootstrap {
private static final Logger log = LoggerFactory.getLogger(GitRepositoryBootstrap.class);
/**
* JVM-wide guard: ensures bootstrap runs at most once per JVM lifetime.
* In production (single context), this fires exactly once. In unit tests
* (multiple cached contexts sharing the JVM-global DfsBlockCache), it
* prevents a second context from committing into a repository whose
* underlying tables were wiped by {@code ddl-auto=create}.
*/
private static final AtomicBoolean BOOTSTRAPPED = new AtomicBoolean(false);
private final DslGitRepository gitRepository;
private final TaxDslExportService exportService;
private final AppInitializationStateService stateService;
public GitRepositoryBootstrap(DslGitRepositoryFactory repositoryFactory,
TaxDslExportService exportService,
AppInitializationStateService stateService) {
this.gitRepository = repositoryFactory.getSystemRepository();
this.exportService = exportService;
this.stateService = stateService;
}
/**
* Creates an initial commit on the {@code draft} branch if it does not already exist.
*
* <p>This method is idempotent: on subsequent restarts with persistent storage,
* the branch already exists and the method returns without making changes.
* The static {@link #BOOTSTRAPPED} guard adds an extra layer of protection
* in multi-context test JVMs.
*/
@EventListener(ApplicationReadyEvent.class)
public void initializeDraftBranch() {
if (!BOOTSTRAPPED.compareAndSet(false, true)) {
log.debug("Draft branch already bootstrapped in this JVM — skipping.");
return;
}
if (!stateService.isReady()) {
log.debug("Taxonomy not yet loaded (async mode) — skipping draft branch bootstrap.");
BOOTSTRAPPED.set(false); // allow retry after taxonomy loads
return;
}
try {
if (gitRepository.getHeadCommit("draft") != null) {
log.debug("Draft branch already exists — skipping bootstrap.");
return;
}
String dsl = exportService.exportAll("default");
String commitId = gitRepository.commitDsl("draft", dsl, "system",
"Initial taxonomy materialization");
log.info("Bootstrapped 'draft' branch with initial taxonomy DSL " +
"(commit={}, {} chars)", commitId.substring(0, 7), dsl.length());
} catch (IOException e) {
log.warn("Failed to bootstrap 'draft' branch: {}", e.getMessage());
}
}
}