diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java index 6d54df7404..fe6ec7c529 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java @@ -88,7 +88,7 @@ public AbstractWorkingDirectoryAccessor getWorkingDirectory() { */ public RevisionAccess getLiveRevision() { CachingRevisionAccess currentRev = versioningManager.getLiveRevision(); - if (cachedLiveRevision == null || !currentRev.getRevisionID().equals(cachedLiveRevision.getRevisionID())) { + if (cachedLiveRevision == null || !currentRev.getRevisionId().equals(cachedLiveRevision.getRevisionId())) { cachedLiveRevision = currentRev; } return cachedLiveRevision; @@ -105,7 +105,7 @@ public RevisionAccess getLiveRevision() { public RevisionAccess getWorkspaceRevision() { CachingRevisionAccess currentRev = versioningManager.getWorkspaceRevision(); if (cachedWorkspaceRevision == null - || !currentRev.getRevisionID().equals(cachedWorkspaceRevision.getRevisionID())) { + || !currentRev.getRevisionId().equals(cachedWorkspaceRevision.getRevisionId())) { cachedWorkspaceRevision = currentRev; } return cachedWorkspaceRevision; diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java index 063d1c816d..4a45ea5dae 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java @@ -1,5 +1,6 @@ package rocks.inspectit.ocelot.file.accessor.git; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.ObjectId; @@ -7,6 +8,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import rocks.inspectit.ocelot.file.FileInfo; import rocks.inspectit.ocelot.file.accessor.AbstractFileAccessor; @@ -15,9 +17,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * Accessor to access specific Git revision/commits. Using this class ensures that all operations will be executed @@ -38,22 +38,157 @@ public class RevisionAccess extends AbstractFileAccessor { /** * Constructor. + * Always resolves the commit. * * @param repository the repository to use * @param revCommit the commit which will be used for the operations */ public RevisionAccess(Repository repository, RevCommit revCommit) { + this(repository, revCommit, true); + } + + /** + * Constructor. + * + * @param repository the repository to use + * @param revCommit the commit which will be used for the operations + * @param resolveTree if true, the partial commit revCommit will be resolved using a RevWalk. + */ + public RevisionAccess(Repository repository, RevCommit revCommit, boolean resolveTree) { this.repository = repository; - this.revCommit = revCommit; + if (resolveTree) { + try (RevWalk revWalk = new RevWalk(repository)) { + //reparse in case of incomplete revCommits, e.g if the RevCommit was received as a parent from another + this.revCommit = revWalk.parseCommit(revCommit.getId()); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + this.revCommit = revCommit; + } } /** * @return a unique ID for this revision */ - public String getRevisionID() { + public String getRevisionId() { return ObjectId.toString(revCommit.getId()); } + /** + * Returns the main parent of this Revision. + * For merge-commits the main parent is the Revision into which the other changes have been merged. + * + * @return the primary parent of this revision or an empty optional if this is a root commit. + */ + public Optional getPreviousRevision() { + if (revCommit.getParentCount() >= 1) { + return Optional.of(new RevisionAccess(repository, revCommit.getParent(0))); + } else { + return Optional.empty(); + } + } + + /** + * @return the name of the author of this revision. + */ + public String getAuthorName() { + return revCommit.getAuthorIdent().getName(); + } + + /** + * Walks the history backwards to the a revision (A) which is a parent of this revision (B) and another given revision (C). + * This method will return a revision which minimizes MAX(distance(A,B), distance(A,C)). + *

+ * Such a common ancestor should exist for all commits, because all branches originate from the root commit of the repo. + * + * @param other the other revision to find a common ancestor with + * + * @return the Revision which is a parent of both revisions. + */ + public RevisionAccess getCommonAncestor(RevisionAccess other) { + // unfortunately RevFilter.MERGE_BASE does not return the best ancestor, + // therefore we perform a BFS ourselves. + Set ownVisited = new HashSet<>(); + Set otherVisited = new HashSet<>(); + Deque openList = new ArrayDeque<>(); + openList.addLast(new Node(true, this)); + openList.addLast(new Node(false, other)); + while (!openList.isEmpty()) { + Node current = openList.removeFirst(); + String id = current.revAccess.getRevisionId(); + if (current.isReachableFromOwn) { + if (otherVisited.contains(id)) { + return current.revAccess; + } + ownVisited.add(id); + } else { + if (ownVisited.contains(id)) { + return current.revAccess; + } + otherVisited.add(id); + } + for (int i = 0; i < current.revAccess.revCommit.getParentCount(); i++) { + RevCommit parent = current.revAccess.revCommit.getParent(i); + openList.addLast(new Node(current.isReachableFromOwn, new RevisionAccess(repository, parent))); + } + } + throw new IllegalStateException("No common ancestor!"); + } + + /** + * Checks if the given file exists in this revision but not in the parent revision. + * + * @param path the path of the file + * + * @return true, if the file was added in this revision. + */ + public boolean isConfigurationFileAdded(String path) { + if (!configurationFileExists(path)) { + return false; + } + Optional parent = getPreviousRevision(); + return !parent.isPresent() || !parent.get().configurationFileExists(path); + } + + /** + * Checks if the given file exists in both this revision and the parent revision, + * but it's contents have changed. + * + * @param path the path of the file + * + * @return true, if the file exists both in this and the parent revision but with different contents. + */ + public boolean isConfigurationFileModified(String path) { + if (!configurationFileExists(path)) { + return false; + } + Optional parent = getPreviousRevision(); + if (!parent.isPresent() || !parent.get().configurationFileExists(path)) { + return false; + } + String currentContent = readConfigurationFile(path) + .orElseThrow(() -> new IllegalStateException("Expected file to exist")); + String previousContent = parent.get().readConfigurationFile(path) + .orElseThrow(() -> new IllegalStateException("Expected file to exist")); + return !currentContent.equals(previousContent); + } + + /** + * Checks if the given file does not exist in this revision but existed in the parent revision. + * + * @param path the path of the file + * + * @return true, if the file was deleted in this revision. + */ + public boolean isConfigurationFileDeleted(String path) { + if (configurationFileExists(path)) { + return false; + } + Optional parent = getPreviousRevision(); + return parent.isPresent() && parent.get().configurationFileExists(path); + } + @Override protected String verifyPath(String relativeBasePath, String relativePath) throws IllegalArgumentException { if (relativePath.startsWith("/")) { @@ -99,7 +234,7 @@ protected boolean exists(String path) { try (TreeWalk treeWalk = TreeWalk.forPath(repository, path, revCommit.getTree())) { return treeWalk != null; } catch (Exception e) { - log.error("Could not read file {} from git repository", path, e); + log.error("Assuming file {} does not exist due to exception", path, e); return false; } } @@ -157,7 +292,9 @@ protected List listFiles(String path) { * * @param treeWalk The {@link TreeWalk} to traverse. * @param resultList the list which will be filled with the found files + * * @return The files within the current tree. + * * @throws IOException in case the repository cannot be read */ private boolean collectFiles(TreeWalk treeWalk, List resultList) throws IOException { @@ -192,4 +329,15 @@ private boolean collectFiles(TreeWalk treeWalk, List resultList) throw return false; } + + /** + * Container used for finding the commonAncestor + */ + @Value + private static class Node { + + boolean isReachableFromOwn; + + RevisionAccess revAccess; + } } 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 f27ee8c77e..b422d7140f 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 @@ -11,6 +11,7 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; @@ -31,10 +32,7 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Duration; -import java.util.ConcurrentModificationException; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -412,6 +410,7 @@ WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, O simpleDiffEntries.forEach(entry -> fillFileContent(entry, liveRevision, workspaceRevision)); } + simpleDiffEntries.forEach(entry -> fillInAuthors(entry, oldCommit, newCommit)); return WorkspaceDiff.builder() .entries(simpleDiffEntries) @@ -420,6 +419,137 @@ WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldCommit, O .build(); } + @VisibleForTesting + void fillInAuthors(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; + case MODIFY: + entry.setAuthors(new ArrayList<>(findModifyingAuthors(entry.getFile(), baseCommit, newCommit))); + break; + case DELETE: + entry.setAuthors(Collections.singletonList(findDeletingAuthor(entry.getFile(), baseCommit, newCommit))); + break; + default: + log.warn("Unsupported change type for author lookup encountered: {}", entry.getType()); + break; + } + } + + /** + * Finds all authors who have modified a file since a certain base revision. + * + * @param file the name of the file to check. + * @param baseCommit A commit on the live branch onto which the newCommit will be merged + * @param newCommit A commit on the workspace branch containing file modifications + * + * @return A list of authors who have modified the file. + */ + private Collection findModifyingAuthors(String file, RevCommit baseCommit, RevCommit newCommit) { + RevisionAccess newRevision = new RevisionAccess(git.getRepository(), newCommit); + RevisionAccess baseRevision = new RevisionAccess(git.getRepository(), baseCommit); + //move "baseRevision" to the last commit where this file was touched (potentially the root commit). + baseRevision = findLastChangingRevision(file, baseRevision); + + Set authors = new HashSet<>(); + String baseContent = baseRevision.readConfigurationFile(file).get(); + //Find all persons who added or modified the file since the last promotion. + RevisionAccess commonAncestor = newRevision.getCommonAncestor(baseRevision); + while (!newRevision.getRevisionId().equals(commonAncestor.getRevisionId())) { + if (newRevision.isConfigurationFileModified(file)) { + authors.add(newRevision.getAuthorName()); + } else if (newRevision.isConfigurationFileAdded(file)) { + authors.add(newRevision.getAuthorName()); + break; //THe file has been added, no need to take previous changes into account + } + newRevision = newRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException("Expected parent to exist")); + if (newRevision.configurationFileExists(file) && + newRevision.readConfigurationFile(file).get().equals(baseContent)) { + break; // we have reached a revision where the content is in the original state, no need to look further + } + } + return authors; + } + + /** + * Walks back in history to the point where the given file was added. + * On the way, all authors which have modifies the file are remembered. + * + * @param file the file to check + * @param newCommit the commit to start looking from, usually on the workspace + * + * @return the list of authors who have modified the file since it's addition including the author adding the file + */ + private Collection findAuthorsSinceAddition(String file, RevCommit newCommit) { + RevisionAccess newRevision = new RevisionAccess(git.getRepository(), newCommit); + Set authors = new HashSet<>(); + //Find all persons who edited the file since it was added + while (!newRevision.isConfigurationFileAdded(file)) { + if (newRevision.isConfigurationFileModified(file)) { + authors.add(newRevision.getAuthorName()); + } + newRevision = newRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException("Expected parent to exist")); + } + authors.add(newRevision.getAuthorName()); //Also add the name of the person who added the file + return authors; + } + + /** + * Finds the most recent revision originating from "newCommit" in which the given file was deleted. + * Does not walk past the common ancestor of "newCommit" and "baseCommit". + *

+ * Returns the author of this revision. + * + * @param file the file to check + * @param baseCommit the commit to comapre agains, usually the live branch + * @param newCommit the commit in which the provided file does not exist anymore, usually on the workspace + * + * @return the author of the revision which is responsible for the deletion. + */ + private String findDeletingAuthor(String file, RevCommit baseCommit, RevCommit newCommit) { + RevisionAccess newRevision = new RevisionAccess(git.getRepository(), newCommit); + RevisionAccess baseRevision = new RevisionAccess(git.getRepository(), baseCommit); + //move "baseRevision" to the last commit where this file was touched (potentially the root commit). + baseRevision = findLastChangingRevision(file, baseRevision); + + RevisionAccess commonAncestor = baseRevision.getCommonAncestor(newRevision); + RevisionAccess previous = commonAncestor; + //find the person who deleted the file most recently + while (!newRevision.getRevisionId().equals(commonAncestor.getRevisionId())) { + if (newRevision.isConfigurationFileDeleted(file)) { + return newRevision.getAuthorName(); + } + previous = newRevision; + newRevision = newRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException("Expected parent to exist")); + } + return previous.getAuthorName(); //in case an amend happened, this will be the correct user + } + + /** + * Walks backwards in history starting at the given revision. + * Stops at the first revision which either adds or modifies the given file. + *

+ * If the provided revision already modified/adds the given file, it is returned unchanged. + * + * @param file the file to look for + * @param baseRevision the starting revision to walk backwards from + * + * @return a revision which either modifies or adds the given file. + */ + private RevisionAccess findLastChangingRevision(String file, RevisionAccess baseRevision) { + while (!baseRevision.isConfigurationFileAdded(file) && !baseRevision.isConfigurationFileModified(file)) { + baseRevision = baseRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException("Expected parent to exist")); + } + return baseRevision; + } + /** * Sets the old and new file content of the specified diff entry using the given revision access instances. * @@ -509,6 +639,14 @@ public void promoteConfiguration(ConfigurationPromotion promotion) throws GitAPI // checkout live branch git.checkout().setName(Branch.LIVE.getBranchName()).call(); + // create an empty merge-commit + git.merge() + .include(workspaceCommitId) + .setCommit(false) + .setFastForward(MergeCommand.FastForwardMode.NO_FF) + .setStrategy(MergeStrategy.OURS) + .call(); + // remove all deleted files if (!removeFiles.isEmpty()) { RmCommand rmCommand = git.rm(); diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java index 2033771fbe..526e335ae7 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java @@ -4,6 +4,9 @@ import lombok.*; import org.eclipse.jgit.diff.DiffEntry; +import java.util.Collections; +import java.util.List; + /** * This class represents a single file diff including all information about it. */ @@ -17,6 +20,7 @@ public class SimpleDiffEntry { * Creates a {@link SimpleDiffEntry} based an a given {@link DiffEntry}. * * @param entry the {@link DiffEntry} to use as basis + * * @return the created {@link SimpleDiffEntry} */ public static SimpleDiffEntry of(DiffEntry entry) { @@ -29,7 +33,6 @@ public static SimpleDiffEntry of(DiffEntry entry) { simpleEntry.setFile(entry.getNewPath()); } - return simpleEntry; } @@ -56,4 +59,10 @@ public static SimpleDiffEntry of(DiffEntry entry) { */ @JsonInclude(JsonInclude.Include.NON_NULL) private String newContent; + + /** + * The list of authors who have modified this file since it was last promoted. + */ + @Builder.Default + private List authors = Collections.emptyList(); } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/WorkspaceDiff.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/WorkspaceDiff.java index 764f00d756..ada1003a6e 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/WorkspaceDiff.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/WorkspaceDiff.java @@ -30,4 +30,12 @@ public class WorkspaceDiff { * The target commit, holding the new filed - representing the working directory. */ private String workspaceCommitId; + + /** + * Specifies whether a 4-eyes principle is enabled or disabled. + * If {@link #canPromoteOwnChanges} is true, a user can promote changes of which he is one of the authors. + * Otherwise, this is not allowed. + */ + @Builder.Default + private boolean canPromoteOwnChanges = true; } diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccessTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccessTest.java index fbedf9cd3e..525648fd1b 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccessTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccessTest.java @@ -2,10 +2,10 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -15,7 +15,6 @@ @ExtendWith(MockitoExtension.class) class RevisionAccessTest { - @InjectMocks private RevisionAccess revisionAccess; @Mock @@ -24,6 +23,11 @@ class RevisionAccessTest { @Mock private RevCommit revCommit; + @BeforeEach + void init() { + revisionAccess = new RevisionAccess(repository, revCommit, false); + } + @Nested class VerifyPath { 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 f8cd1fce1e..f8eaa8b4b0 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 @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.Optional; import static org.assertj.core.api.Assertions.*; @@ -385,9 +386,12 @@ public void getDiff() throws IOException, GitAPIException { WorkspaceDiff result = versioningManager.getWorkspaceDiffWithoutContent(); assertThat(result.getEntries()).containsExactlyInAnyOrder( - SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD).build(), - SimpleDiffEntry.builder().file("/file_modified.yml").type(DiffEntry.ChangeType.MODIFY).build(), - SimpleDiffEntry.builder().file("/file_removed.yml").type(DiffEntry.ChangeType.DELETE).build() + SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD) + .authors(Collections.singletonList("user")).build(), + SimpleDiffEntry.builder().file("/file_modified.yml").type(DiffEntry.ChangeType.MODIFY) + .authors(Collections.singletonList("user")).build(), + SimpleDiffEntry.builder().file("/file_removed.yml").type(DiffEntry.ChangeType.DELETE) + .authors(Collections.singletonList("user")).build() ); assertThat(result.getLiveCommitId()).isNotEqualTo(result.getWorkspaceCommitId()); } @@ -410,17 +414,20 @@ public void getDiffWithContent() throws IOException, GitAPIException { .file("/file_added.yml") .type(DiffEntry.ChangeType.ADD) .newContent("") + .authors(Collections.singletonList("user")) .build(), SimpleDiffEntry.builder() .file("/file_modified.yml") .type(DiffEntry.ChangeType.MODIFY) .oldContent("") .newContent("new content") + .authors(Collections.singletonList("user")) .build(), SimpleDiffEntry.builder() .file("/file_removed.yml") .type(DiffEntry.ChangeType.DELETE) .oldContent("content") + .authors(Collections.singletonList("user")) .build() ); assertThat(result.getLiveCommitId()).isNotEqualTo(result.getWorkspaceCommitId()); @@ -450,6 +457,7 @@ public void getDiffById() throws IOException, GitAPIException { .type(DiffEntry.ChangeType.MODIFY) .oldContent("") .newContent("new content") + .authors(Collections.singletonList("user")) .build() ); assertThat(resultFirst.getLiveCommitId()).isEqualTo(liveId.name()); @@ -461,6 +469,7 @@ public void getDiffById() throws IOException, GitAPIException { .type(DiffEntry.ChangeType.MODIFY) .oldContent("") .newContent("another content") + .authors(Collections.singletonList("user")) .build() ); assertThat(resultSecond.getLiveCommitId()).isEqualTo(liveId.name()); @@ -528,7 +537,8 @@ public void partialPromotion() throws GitAPIException, IOException { WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); assertThat(diff.getEntries()).containsExactlyInAnyOrder( - SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD).build() + SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD) + .authors(Collections.singletonList("user")).build() ); } @@ -577,8 +587,10 @@ public void multiplePromotions() throws GitAPIException, IOException { WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); assertThat(diff.getEntries()).containsExactlyInAnyOrder( - SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD).build(), - SimpleDiffEntry.builder().file("/file_removed.yml").type(DiffEntry.ChangeType.DELETE).build() + SimpleDiffEntry.builder().file("/file_added.yml").type(DiffEntry.ChangeType.ADD) + .authors(Collections.singletonList("user")).build(), + SimpleDiffEntry.builder().file("/file_removed.yml").type(DiffEntry.ChangeType.DELETE) + .authors(Collections.singletonList("user")).build() ); } @@ -618,7 +630,7 @@ public void differentLiveBranch() throws GitAPIException, IOException { } @Test - public void promotionWithModifictaion() throws GitAPIException, IOException { + public void promotionWithModification() throws GitAPIException, IOException { createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_modified.yml"); versioningManager.initialize(); createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_modified.yml=content_A"); @@ -643,7 +655,8 @@ public void promotionWithModifictaion() throws GitAPIException, IOException { WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); assertThat(diff.getEntries()).containsExactlyInAnyOrder( - SimpleDiffEntry.builder().file("/file_modified.yml").type(DiffEntry.ChangeType.MODIFY).build() + SimpleDiffEntry.builder().file("/file_modified.yml").type(DiffEntry.ChangeType.MODIFY) + .authors(Collections.singletonList("user")).build() ); assertThat(versioningManager.getLiveRevision() .readConfigurationFile("file_modified.yml")).hasValue("content_A"); @@ -689,4 +702,244 @@ void activeUserUsed() { assertThat(vm.getCurrentAuthor().getName()).isEqualTo(authentication.getName()); } } + + @Nested + class FillInAuthors { + + private void promote(String... files) throws Exception { + 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(files)); + versioningManager.promoteConfiguration(promotion); + } + + private void buildDummyHistory() throws Exception { + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/c0_file_a=content"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/c0_file_b.yml"); + versioningManager.initialize(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/c1_file_a.yml=content"); + doReturn("user_a").when(authentication).getName(); + versioningManager.commitAllChanges("new commit"); + promote( + "/c0_file_a.yml", + "/c0_file_b.yml", + "/c1_file_a.yml" + ); + + Files.delete(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/c0_file_b.yml")); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/c1_file_a.yml=newFileContent"); + + doReturn("user_b").when(authentication).getName(); + versioningManager.commitAllChanges("new commit"); + promote( + "/c0_file_b.yml", + "/c1_file_a.yml" + ); + } + + @Test + void fileAddedInMostRecentChange() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml"); + doReturn("creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("new commit"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.ADD) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("creating_user"); + } + + @Test + void fileAddedAndModifiedBeforeLastPromotion() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml"); + doReturn("creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("new commit"); + promote("/independent_file.yml"); + + doReturn("editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=content_modified"); + versioningManager.commitAllChanges("new commit"); + + doReturn("independent_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml=modified"); + versioningManager.commitAllChanges("new commit"); + promote("/independent_file.yml"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.ADD) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("creating_user", "editing_user"); + } + + @Test + void fileModified() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml"); + doReturn("initial_creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("com1"); + promote("/independent_file.yml", "/new_file.yml"); + + doReturn("editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=content_modified"); + versioningManager.commitAllChanges("com2"); + + doReturn("deleting_user").when(authentication).getName(); + Files.delete(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml")); + versioningManager.commitAllChanges("com3"); + + doReturn("creating_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=new_initial_content"); + versioningManager.commitAllChanges("com4"); + + doReturn("independent_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml=modified"); + versioningManager.commitAllChanges("com5"); + promote("/independent_file.yml"); + + doReturn("second_editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=content_modified"); + versioningManager.commitAllChanges("com6"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.MODIFY) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("creating_user", "second_editing_user"); + } + + @Test + void fileModifiedWithChangesUndone() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=initialContent"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml"); + doReturn("initial_creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("com1"); + promote("/independent_file.yml", "/new_file.yml"); + + doReturn("editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=newContent"); + versioningManager.commitAllChanges("com2"); + + doReturn("undoing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=initialContent"); + versioningManager.commitAllChanges("com3"); + + doReturn("last_editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=superNewContent"); + versioningManager.commitAllChanges("com4"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.MODIFY) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("last_editing_user"); + } + + @Test + void fileDeleted() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml"); + doReturn("initial_creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("com1"); + promote("/independent_file.yml", "/new_file.yml"); + + doReturn("editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml=content_modified"); + versioningManager.commitAllChanges("com2"); + + doReturn("independent_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml=modified"); + versioningManager.commitAllChanges("com3"); + promote("/independent_file.yml"); + + doReturn("deleting_user").when(authentication).getName(); + Files.delete(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml")); + versioningManager.commitAllChanges("com4"); + ObjectId deleting_com4 = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId(); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.DELETE) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("deleting_user"); + } + + @Test + void fileDeletionAmended() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml"); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml"); + doReturn("deleting_user").when(authentication).getName(); + versioningManager.commitAllChanges("com1"); + promote("/independent_file.yml", "/new_file.yml"); + + doReturn("deleting_user").when(authentication).getName(); + Files.delete(tempDirectory.resolve(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/new_file.yml")); + versioningManager.commitAllChanges("com2"); + + doReturn("independent_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/independent_file.yml=modified"); + versioningManager.commitAllChanges("com3"); + promote("/independent_file.yml"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file("new_file.yml") + .type(DiffEntry.ChangeType.DELETE) + .build(); + + versioningManager.fillInAuthors(diff, + versioningManager.getLatestCommit(Branch.LIVE).get().getId(), + versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(diff.getAuthors()) + .containsExactlyInAnyOrder("deleting_user"); + } + } } \ No newline at end of file diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java index 0b664b9272..c95a5f8f97 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java @@ -36,7 +36,9 @@ public void getPromotionFiles() { ResponseEntity result = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(result.getBody().getEntries()).containsExactly(SimpleDiffEntry.builder().file("/src/file.yml").type(DiffEntry.ChangeType.ADD).build()); + assertThat(result.getBody().getEntries()) + .containsExactly(SimpleDiffEntry.builder().file("/src/file.yml").type(DiffEntry.ChangeType.ADD) + .authors(Collections.singletonList(settings.getDefaultUser().getName())).build()); } }