From 50a2c115bdffba028cff8af5210bb5547c850244 Mon Sep 17 00:00:00 2001 From: Henning Schulz Date: Thu, 11 Nov 2021 15:08:38 +0100 Subject: [PATCH] Closes #1209 - Live branch is synchronized with remote target branch to allow for pushing without force (#1210) --- .../RemoteConfigurationManager.java | 31 ++-- .../file/versioning/VersioningManager.java | 155 +++++++++++++++--- .../versioning/VersioningManagerTest.java | 147 +++++++++++++++-- .../config-server/configuration-staging.md | 6 + 4 files changed, 296 insertions(+), 43 deletions(-) diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/RemoteConfigurationManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/RemoteConfigurationManager.java index 122cc86e20..a806de1939 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/RemoteConfigurationManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/RemoteConfigurationManager.java @@ -162,16 +162,7 @@ public void fetchSourceBranch(RemoteRepositorySettings sourceRepository) throws log.info("Fetching branch '{}' from configuration remote '{}'.", sourceRepository.getBranchName(), sourceRepository .getRemoteName()); - LsRemoteCommand lsRemoteCommand = git.lsRemote().setRemote(sourceRepository.getRemoteName()); - authenticateCommand(lsRemoteCommand, sourceRepository); - - Collection refs = lsRemoteCommand.call(); - - Optional sourceBranch = refs.stream() - .filter(ref -> ref.getName().equals("refs/heads/" + sourceRepository.getBranchName())) - .findAny(); - - if (!sourceBranch.isPresent()) { + if (!sourceBranchExistsOnRemote(sourceRepository)) { throw new IllegalStateException(String.format("Specified configuration source branch '%s' does not exists on remote '%s'.", sourceRepository .getBranchName(), sourceRepository.getRemoteName())); } @@ -192,6 +183,26 @@ public void fetchSourceBranch(RemoteRepositorySettings sourceRepository) throws } } + /** + * Checks whether the branch represented by the given {@link RemoteRepositorySettings} exists on the remote repository. + * + * @param sourceRepository the definition of the remote repository and branch + * + * @return {@code true} if the remote repository has the branch or {@code false} otherwise + */ + public boolean sourceBranchExistsOnRemote(RemoteRepositorySettings sourceRepository) throws GitAPIException { + LsRemoteCommand lsRemoteCommand = git.lsRemote().setRemote(sourceRepository.getRemoteName()); + authenticateCommand(lsRemoteCommand, sourceRepository); + + Collection refs = lsRemoteCommand.call(); + + Optional sourceBranch = refs.stream() + .filter(ref -> ref.getName().equals("refs/heads/" + sourceRepository.getBranchName())) + .findAny(); + + return sourceBranch.isPresent(); + } + /** * Injects a {@link CredentialsProvider} for executing a user-password authentication. This is used for HTTP(s)-remotes. * diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java index 178c0df8c7..e8a9cd07c0 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java @@ -34,7 +34,9 @@ import rocks.inspectit.ocelot.file.versioning.model.WorkspaceVersion; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.*; import java.util.function.Supplier; @@ -126,9 +128,22 @@ public synchronized void initialize() throws GitAPIException, IOException { git = Git.init().setDirectory(workingDirectory.toFile()).call(); + RemoteConfigurationsSettings remoteSettings = settings.getRemoteConfigurations(); + boolean usingRemoteConfiguration = remoteSettings != null && remoteSettings.isEnabled(); + + if (usingRemoteConfiguration) { + initRemoteConfigurationManager(); + } + if (!hasGit) { log.info("Working directory is not managed by Git. Initializing Git repository and staging and committing all existing file."); + if (usingRemoteConfiguration && remoteSettings.getPushRepository() != null && remoteConfigurationManager.sourceBranchExistsOnRemote(remoteSettings + .getPushRepository())) { + // we will need to push new commits to target branch in this case; thus, make sure it can be done without force + setCurrentBranchToTarget(); + } + stageFiles(); commitAllFiles(GIT_SYSTEM_AUTHOR, "Initializing Git repository using existing working directory", false); @@ -151,14 +166,7 @@ public synchronized void initialize() throws GitAPIException, IOException { commitAllFiles(GIT_SYSTEM_AUTHOR, "Staging and committing of external changes during startup", false); } - RemoteConfigurationsSettings remoteSettings = settings.getRemoteConfigurations(); - if (remoteSettings != null && remoteSettings.isEnabled()) { - // init RemoteConfigurationManager - remoteConfigurationManager = new RemoteConfigurationManager(settings, git); - - // update remote refs in case they are configured - remoteConfigurationManager.updateRemoteRefs(); - + if (usingRemoteConfiguration) { // push the current state during startup if (remoteSettings.isPushAtStartup() && remoteSettings.getPushRepository() != null) { remoteConfigurationManager.pushBranch(Branch.LIVE, remoteSettings.getPushRepository()); @@ -172,6 +180,83 @@ public synchronized void initialize() throws GitAPIException, IOException { } } + /** + * Inits RemoteConfigurationManager and updates remote refs in case they are configured. + */ + private void initRemoteConfigurationManager() throws GitAPIException { + if (remoteConfigurationManager == null) { + remoteConfigurationManager = new RemoteConfigurationManager(settings, git); + remoteConfigurationManager.updateRemoteRefs(); + } + } + + /** + * Sets the currently checked-out branch on top of the remote target branch using {@code git reset}. + * There are two cases: + * In case it can be foreseen that the desired state after initialization is that workspace, live, and both remote + * branches (source/target) are all equal, it does a hard reset. This is the case if there are no local files before + * the git initialization, initial configuration synchronization and auto promotion are active, and source and target + * remote branches have the same latest commit. After calling this method, the local branch will then be equal to + * the target/source branches. + *

+ * Otherwise, it uses soft reset, resulting in a commit history including the target branch's history and another + * commit that resets everything to the state of the local files. Merging of the files from the source branch has to + * be done afterwards. + *

+ * Expects the target branch to be present (in configuration and remote push repository). + */ + private void setCurrentBranchToTarget() throws GitAPIException, IOException { + log.info("Synchronizing local live branch with target branch of remote push repository."); + + RemoteConfigurationsSettings remoteSettings = settings.getRemoteConfigurations(); + RemoteRepositorySettings sourceRepository = settings.getRemoteConfigurations().getPullRepository(); + RemoteRepositorySettings targetRepository = settings.getRemoteConfigurations().getPushRepository(); + + // fetch pull and push repo, as we will need both to compare and synchronize local/pull/push repos + // push repo is expected to exist + remoteConfigurationManager.fetchSourceBranch(targetRepository); + if (sourceRepository != null) { + remoteConfigurationManager.fetchSourceBranch(sourceRepository); + } + + // this is true for a fresh start of a config server instance connected to a remote git for backup + // --> prevent unnecessary commits at startup that don't change any files (particularly when frequently restarting in, e.g., Kubernetes) + // otherwise, we need to properly merge files from local and two remotes + boolean hardReset = isWorkingDirectoryEmpty() && remoteSettings.isInitialConfigurationSync() && remoteSettings.isAutoPromotion() && areRemotesEqual(sourceRepository, targetRepository); + + log.info("{}-resetting current branch to '{}'.", (hardReset ? "Hard" : "Soft"), targetRepository.getBranchName()); + git.reset() + .setRef("refs/heads/" + targetRepository.getBranchName()) + .setMode(hardReset ? ResetCommand.ResetType.HARD : ResetCommand.ResetType.SOFT) + .call(); + + log.info("Local changes can now be pushed to the remote target branch without force."); + } + + private boolean areRemotesEqual(RemoteRepositorySettings sourceRepository, RemoteRepositorySettings targetRepository) throws IOException { + if (sourceRepository == null || targetRepository == null) { + return sourceRepository == targetRepository; // true iff both are null + } + + Repository repository = git.getRepository(); + + ObjectId sourceId = repository.exactRef("refs/heads/" + sourceRepository.getBranchName()).getObjectId(); + ObjectId targetId = repository.exactRef("refs/heads/" + targetRepository.getBranchName()).getObjectId(); + + return ObjectId.isEqual(sourceId, targetId); + } + + private boolean isWorkingDirectoryEmpty() throws IOException { + Path filesPath = Paths.get(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER); + boolean filesEmpty = !Files.exists(filesPath) || (Files.isDirectory(filesPath) && Files.list(filesPath) + .count() == 0); + + Path agentMappingPath = Paths.get(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); + boolean agentMappingMissing = !Files.exists(agentMappingPath); + + return filesEmpty && agentMappingMissing; + } + /** * Closes the {@link #git} instance of this manager. */ @@ -438,6 +523,20 @@ public WorkspaceDiff getWorkspaceDiff(boolean includeFileContent) throws IOExcep */ @VisibleForTesting WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, ObjectId newCommit) throws IOException, GitAPIException { + return getWorkspaceDiff(includeFileContent, oldCommit, newCommit, null); + } + + /** + * See {@link #getWorkspaceDiff(boolean, ObjectId, ObjectId, PersonIdent)}. + * + * @param includeFileContent whether the file difference (old and new content) is included + * @param oldCommit the commit id of the base (old) commit + * @param newCommit the commit id of the target (new) commit + * @param deletingAuthor the author to be set for all file deletions, instead of detected authors (required if {@code oldCommit} and {@code newCommit} don't have a common ancestor) + * + * @return the diff between the specified branches + */ + private WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, ObjectId newCommit, PersonIdent deletingAuthor) throws IOException, GitAPIException { // the diff works on TreeIterators, we prepare two for the two branches AbstractTreeIterator oldTree = prepareTreeParser(oldCommit); AbstractTreeIterator newTree = prepareTreeParser(newCommit); @@ -459,7 +558,17 @@ WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, O simpleDiffEntries.forEach(entry -> fillFileContent(entry, liveRevision, workspaceRevision)); } - simpleDiffEntries.forEach(entry -> fillInAuthors(entry, oldCommit, newCommit)); + + // fill in the file's authors who did a modification to it + simpleDiffEntries.forEach(entry -> { + List authors; + if (deletingAuthor != null && entry.getType() == DiffEntry.ChangeType.DELETE) { + authors = Collections.singletonList(deletingAuthor.getName()); + } else { + authors = getModifyingAuthors(entry, oldCommit, newCommit); + } + entry.setAuthors(authors); + }); return WorkspaceDiff.builder() .entries(simpleDiffEntries) @@ -469,22 +578,19 @@ WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, O } @VisibleForTesting - void fillInAuthors(SimpleDiffEntry entry, ObjectId baseCommitId, ObjectId newCommitId) { + List getModifyingAuthors(SimpleDiffEntry entry, ObjectId baseCommitId, ObjectId newCommitId) { RevCommit baseCommit = getCommit(baseCommitId); RevCommit newCommit = getCommit(newCommitId); switch (entry.getType()) { case ADD: - entry.setAuthors(new ArrayList<>(findAuthorsSinceAddition(entry.getFile(), newCommit))); - break; + return new ArrayList<>(findAuthorsSinceAddition(entry.getFile(), newCommit)); case MODIFY: - entry.setAuthors(new ArrayList<>(findModifyingAuthors(entry.getFile(), baseCommit, newCommit))); - break; + return new ArrayList<>(findModifyingAuthors(entry.getFile(), baseCommit, newCommit)); case DELETE: - entry.setAuthors(Collections.singletonList(findDeletingAuthor(entry.getFile(), baseCommit, newCommit))); - break; + return Collections.singletonList(findDeletingAuthor(entry.getFile(), baseCommit, newCommit)); default: log.warn("Unsupported change type for author lookup encountered: {}", entry.getType()); - break; + return Collections.emptyList(); } } @@ -895,10 +1001,11 @@ synchronized void mergeSourceBranch() throws GitAPIException, IOException { */ private void mergeBranch(ObjectId baseObject, ObjectId targetObject, boolean removeFiles, String commitMessage) throws IOException, GitAPIException { // getting the diff of the base and target commit - WorkspaceDiff diff = getWorkspaceDiff(false, baseObject, targetObject); - if (diff.getEntries().isEmpty()) { - log.info("There is nothing to merge from the source configuration branch into the current workspace branch."); - return; + WorkspaceDiff diff; + if (removeFiles) { + diff = getWorkspaceDiff(false, baseObject, targetObject); + } else { + diff = getWorkspaceDiff(false, baseObject, targetObject, GIT_SYSTEM_AUTHOR); } // collect diff files @@ -919,6 +1026,11 @@ private void mergeBranch(ObjectId baseObject, ObjectId targetObject, boolean rem .map(this::prefixRelativeFile) .collect(Collectors.toList()); + if (fileToRemove.isEmpty() && checkoutFiles.isEmpty()) { + log.info("There is nothing to merge from the source configuration branch into the current workspace branch."); + return; + } + // merge target commit into current branch mergeFiles(targetObject, checkoutFiles, fileToRemove, commitMessage, GIT_SYSTEM_AUTHOR); @@ -939,6 +1051,7 @@ private void mergeBranch(ObjectId baseObject, ObjectId targetObject, boolean rem promoteConfiguration(promotion, true, GIT_SYSTEM_AUTHOR); } + } /** diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java index 671110c20c..59e6c98c2f 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java @@ -821,11 +821,11 @@ void fileAddedInMostRecentChange() throws Exception { .type(DiffEntry.ChangeType.ADD) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("creating_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user"); } @Test @@ -852,11 +852,11 @@ void fileAddedAndModifiedBeforeLastPromotion() throws Exception { .type(DiffEntry.ChangeType.ADD) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("creating_user", "editing_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user", "editing_user"); } @Test @@ -895,11 +895,11 @@ void fileModified() throws Exception { .type(DiffEntry.ChangeType.MODIFY) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("creating_user", "second_editing_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user", "second_editing_user"); } @Test @@ -929,11 +929,11 @@ void fileModifiedWithChangesUndone() throws Exception { .type(DiffEntry.ChangeType.MODIFY) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("last_editing_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("last_editing_user"); } @Test @@ -965,11 +965,11 @@ void fileDeleted() throws Exception { .type(DiffEntry.ChangeType.DELETE) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("deleting_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("deleting_user"); } @Test @@ -996,11 +996,11 @@ void fileDeletionAmended() throws Exception { .type(DiffEntry.ChangeType.DELETE) .build(); - versioningManager.fillInAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) .get() .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); - assertThat(diff.getAuthors()).containsExactlyInAnyOrder("deleting_user"); + assertThat(modifyingAuthors).containsExactlyInAnyOrder("deleting_user"); } } @@ -1183,5 +1183,128 @@ public void mergeBranch() throws URISyntaxException, GitAPIException, IOExceptio assertThat(liveCommit.getParents()).extracting(RevObject::getId) // the latest live commit has two parent commits: the old live one and the latest workspace one .containsExactlyInAnyOrder(liveIdBefore, commitAfter.getId()); } + + @Test + public void initialConfigurationSyncOneRemote() throws URISyntaxException, GitAPIException, IOException { + RemoteRepositorySettings remoteRepositorySettings = RemoteRepositorySettings.builder() + .branchName("remote") + .remoteName("remote") + .gitRepositoryUri(new URIish(directoryOne.toString() + "/.git")) + .useForcePush(false) + .build(); + RemoteConfigurationsSettings configurationsSettings = RemoteConfigurationsSettings.builder() + .enabled(true) + .pullAtStartup(true) + .initialConfigurationSync(true) + .pullRepository(remoteRepositorySettings) + .pushRepository(remoteRepositorySettings) + .build(); + when(settings.getRemoteConfigurations()).thenReturn(configurationsSettings); + + // prepare remote Git + addEmptyFile(repositoryOne, "from_remote.yml"); + repositoryOne.add().addFilepattern("files/from_remote.yml").call(); + repositoryOne.commit().setMessage("init remote").call(); + repositoryOne.branchCreate().setName("remote").call(); + repositoryOne.checkout().setName("remote").call(); + + ObjectId targetCommitIdBeforeInitialization = repositoryOne.getRepository() + .exactRef("refs/heads/remote") + .getObjectId(); + + // initialize Git + versioningManager.initialize(); + Git git = (Git) ReflectionTestUtils.getField(versioningManager, "git"); + + // //////////////////////// + // check state after initial synchronization + ObjectId liveCommitId = versioningManager.getLatestCommit(Branch.LIVE).get().getId(); + + assertThat(liveCommitId).isEqualTo(targetCommitIdBeforeInitialization); // live should be set to remote repo + assertThat(Files.list(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER))).extracting(Path::getFileName) + .extracting(Path::toString) + .containsExactlyInAnyOrder("from_remote.yml"); // should exactly contain content of remote repo + } + + @Test + public void initialConfigurationSyncTwoRemotes() throws URISyntaxException, GitAPIException, IOException { + RemoteRepositorySettings pullRepositorySettings = RemoteRepositorySettings.builder() + .branchName("pull") + .remoteName("pull") + .gitRepositoryUri(new URIish(directoryOne.toString() + "/.git")) + .build(); + RemoteRepositorySettings pushRepositorySettings = RemoteRepositorySettings.builder() + .branchName("push") + .remoteName("push") + .gitRepositoryUri(new URIish(directoryTwo.toString() + "/.git")) + .useForcePush(false) + .build(); + RemoteConfigurationsSettings configurationsSettings = RemoteConfigurationsSettings.builder() + .enabled(true) + .pullAtStartup(true) + .initialConfigurationSync(true) + .pullRepository(pullRepositorySettings) + .pushRepository(pushRepositorySettings) + .build(); + when(settings.getRemoteConfigurations()).thenReturn(configurationsSettings); + + // prepare pull Git + addEmptyFile(repositoryOne, "from_pull.yml"); + repositoryOne.add().addFilepattern("files/from_pull.yml").call(); + repositoryOne.commit().setMessage("init pull").call(); + repositoryOne.branchCreate().setName("pull").call(); + repositoryOne.checkout().setName("pull").call(); + + // prepare push Git + addEmptyFile(repositoryTwo, "empty.yml"); + repositoryTwo.add().addFilepattern("files/empty.yml").call(); + repositoryTwo.commit().setMessage("init push").call(); + repositoryTwo.branchCreate().setName("push").call(); + repositoryTwo.checkout().setName("push").call(); + + // add local file + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/from_local.yml=existed_before_git_init"); + + // initialize Git + versioningManager.initialize(); + Git git = (Git) ReflectionTestUtils.getField(versioningManager, "git"); + + // //////////////////////// + // check state after initial synchronization + ObjectId targetCommitId = repositoryTwo.getRepository().exactRef("refs/heads/push").getObjectId(); + ObjectId liveCommitId = versioningManager.getLatestCommit(Branch.LIVE).get().getId(); + + assertThat(liveCommitId).isEqualTo(targetCommitId); // live and target should be synchronized now (live pushed to target) + assertThat(Files.list(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER))).extracting(Path::getFileName) + .extracting(Path::toString) + .containsExactlyInAnyOrder("from_pull.yml", "from_local.yml"); // should contain all files from pull repo and local + + // //////////////////////// + // add files and push (without force) + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/foobar.yml=content"); + versioningManager.commitAllChanges("add foobar"); + + String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); + String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); + + ConfigurationPromotion promotion = new ConfigurationPromotion(); + promotion.setLiveCommitId(liveId); + promotion.setWorkspaceCommitId(workspaceId); + promotion.setFiles(Arrays.asList("/foobar.yml")); + + PromotionResult promotionResult = versioningManager.promoteConfiguration(promotion, true); + + assertThat(promotionResult).isEqualTo(PromotionResult.OK); // OK means no synchronization error + } + + private void addEmptyFile(Git repo, String filename) throws IOException { + Path filesDir = repo.getRepository() + .getWorkTree() + .toPath() + .resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER); + Files.createDirectories(filesDir); + Files.createFile(filesDir.resolve(filename)); + } } } \ No newline at end of file diff --git a/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md b/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md index c8fe064637..a71c800323 100644 --- a/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md +++ b/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md @@ -94,6 +94,12 @@ Note that by default, pushing the branch is **forced**, which could inadvertentl This process can also be triggered by an automatic promotion due to pulling configuration files from a remote Git repository. +:::note +When the configuration server initialized its Git repository for the first time, the `LIVE` branch continues the commit history of the specified branch of the push repository. +As a result, new commits can be pushed unforced, as long as the push repository has no external changes. +**Only the commit history is adopted**, while the file content remains as before the Git initialization. +::: + ## Triggering File Pulling Using Webhooks As already mentioned, pulling configuration files from a remote Git repository can be done at startup of the configuration server, or can be triggered by a webhook.