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 com.taxonomy.shared.service.AppInitializationStateService.InitializationReadyEvent;
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, the application-ready
* event can precede catalogue completion; {@link InitializationReadyEvent} retries the
* same idempotent bootstrap immediately after the catalogue becomes authoritative.
*
* <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() {
// Check readiness before acquiring the one-shot guard. Otherwise an
// ApplicationReady listener can hold the guard while the asynchronous
// READY event is dispatched, causing that only retry to be skipped.
if (!stateService.isReady()) {
log.debug("Taxonomy not yet loaded — deferring draft branch bootstrap.");
return;
}
if (!BOOTSTRAPPED.compareAndSet(false, true)) {
log.debug("Draft branch already bootstrapped in this JVM — skipping.");
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 | RuntimeException error) {
BOOTSTRAPPED.set(false);
log.warn("Failed to bootstrap 'draft' branch: {}", error.getMessage());
}
}
/** Completes the deferred bootstrap after asynchronous taxonomy loading. */
@EventListener(InitializationReadyEvent.class)
public void initializeAfterTaxonomyReady() {
initializeDraftBranch();
}
}