From 5df9c7b6f1dda7c83a9123e7fc60930e8597f60b Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Fri, 30 Jan 2026 15:02:36 +0100 Subject: [PATCH] feat: improve git security settings --- .../civisibility/git/tree/ShellGitClient.java | 277 +++++++++--------- .../git/tree/GitClientTest.groovy | 123 +++++--- 2 files changed, 228 insertions(+), 172 deletions(-) diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java index 20584700300..16d6dcb3bd5 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java @@ -52,6 +52,7 @@ public class ShellGitClient implements GitClient { private final String latestCommitsSince; private final int latestCommitsLimit; private final ShellCommandExecutor commandExecutor; + private final String safeDirectoryOption; /** * Creates a new git client @@ -71,10 +72,54 @@ public class ShellGitClient implements GitClient { int latestCommitsLimit, long timeoutMillis) { this.metricCollector = metricCollector; - this.repoRoot = repoRoot; this.latestCommitsSince = latestCommitsSince; this.latestCommitsLimit = latestCommitsLimit; - commandExecutor = new ShellCommandExecutor(new File(repoRoot), timeoutMillis); + + // Find actual git repo root by traversing upward to find .git + String gitRepoRoot = findGitRepositoryRoot(new File(repoRoot).getAbsoluteFile()); + this.repoRoot = gitRepoRoot; + this.safeDirectoryOption = "safe.directory=" + gitRepoRoot; + commandExecutor = new ShellCommandExecutor(new File(gitRepoRoot), timeoutMillis); + LOGGER.debug("Git safe directory configured to {}", gitRepoRoot); + } + + /** + * Finds the Git repository root by traversing upward from the given directory looking for a .git + * directory or file (for worktrees). + * + * @param startDir The directory to start searching from + * @return The canonical path to the repository root, or the original path if no .git is found + */ + private static String findGitRepositoryRoot(File startDir) { + try { + File current = startDir.getCanonicalFile(); + while (current != null) { + File gitDir = new File(current, ".git"); + if (gitDir.exists()) { + return current.getPath(); + } + current = current.getParentFile(); + } + } catch (IOException e) { + LOGGER.debug("Could not get canonical path for {}", startDir, e); + } + // If no .git found or error occurred, return the original directory as fallback + return startDir.getAbsolutePath(); + } + + /** + * Builds a git command with the {@code safe.directory} option. + * + * @param gitArgs The git command arguments (everything after "git") + * @return The complete command array including "git", "-c", "safe.directory=...", and the args + */ + String[] buildGitCommand(String... gitArgs) { + String[] command = new String[gitArgs.length + 3]; + command[0] = "git"; + command[1] = "-c"; + command[2] = safeDirectoryOption; + System.arraycopy(gitArgs, 0, command, 3, gitArgs.length); + return command; } /** @@ -93,7 +138,8 @@ public boolean isShallow() throws IOException, TimeoutException, InterruptedExce () -> { String output = commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "--is-shallow-repository") + .executeCommand( + IOUtils::readFully, buildGitCommand("rev-parse", "--is-shallow-repository")) .trim(); return Boolean.parseBoolean(output); }); @@ -119,7 +165,7 @@ public String getUpstreamBranchSha() throws IOException, TimeoutException, Inter Command.OTHER, () -> commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "@{upstream}") + .executeCommand(IOUtils::readFully, buildGitCommand("rev-parse", "@{upstream}")) .trim()); } @@ -147,24 +193,24 @@ public void unshallow(@Nullable String remoteCommitReference) String commitSha = getSha(remoteCommitReference); commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - "--update-shallow", - "--filter=blob:none", - "--recurse-submodules=no", - String.format("--shallow-since='%s'", latestCommitsSince), - remote, - commitSha); + buildGitCommand( + "fetch", + "--update-shallow", + "--filter=blob:none", + "--recurse-submodules=no", + String.format("--shallow-since='%s'", latestCommitsSince), + remote, + commitSha)); } else { commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - "--update-shallow", - "--filter=blob:none", - "--recurse-submodules=no", - String.format("--shallow-since='%s'", latestCommitsSince), - remote); + buildGitCommand( + "fetch", + "--update-shallow", + "--filter=blob:none", + "--recurse-submodules=no", + String.format("--shallow-since='%s'", latestCommitsSince), + remote)); } return (Void) null; @@ -187,7 +233,8 @@ public String getGitFolder() throws IOException, TimeoutException, InterruptedEx Command.OTHER, () -> commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "--absolute-git-dir") + .executeCommand( + IOUtils::readFully, buildGitCommand("rev-parse", "--absolute-git-dir")) .trim()); } @@ -207,7 +254,7 @@ public String getRepoRoot() throws IOException, TimeoutException, InterruptedExc Command.OTHER, () -> commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "--show-toplevel") + .executeCommand(IOUtils::readFully, buildGitCommand("rev-parse", "--show-toplevel")) .trim()); } @@ -233,7 +280,8 @@ public String getRemoteUrl(String remoteName) () -> commandExecutor .executeCommand( - IOUtils::readFully, "git", "config", "--get", "remote." + remoteName + ".url") + IOUtils::readFully, + buildGitCommand("config", "--get", "remote." + remoteName + ".url")) .trim()); } @@ -253,7 +301,7 @@ public String getCurrentBranch() throws IOException, TimeoutException, Interrupt Command.GET_BRANCH, () -> commandExecutor - .executeCommand(IOUtils::readFully, "git", "branch", "--show-current") + .executeCommand(IOUtils::readFully, buildGitCommand("branch", "--show-current")) .trim()); } @@ -279,7 +327,8 @@ public List getTags(String commit) () -> { try { return commandExecutor.executeCommand( - IOUtils::readLines, "git", "describe", "--tags", "--exact-match", commit); + IOUtils::readLines, + buildGitCommand("describe", "--tags", "--exact-match", commit)); } catch (ShellCommandExecutor.ShellCommandFailedException e) { // if provided commit is not tagged, // command will fail because "--exact-match" is specified @@ -309,7 +358,7 @@ public String getSha(String reference) Command.OTHER, () -> commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", reference) + .executeCommand(IOUtils::readFully, buildGitCommand("rev-parse", reference)) .trim()); } @@ -325,10 +374,7 @@ private boolean isCommitPresent(String commitReference) try { commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "cat-file", - "-e", - commitReference + "^{commit}"); + buildGitCommand("cat-file", "-e", commitReference + "^{commit}")); return true; } catch (ShellCommandExecutor.ShellCommandFailedException ignored) { return false; @@ -348,13 +394,13 @@ private void fetchCommit(String remoteCommitReference) String remote = getRemoteName(); commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - "--filter=blob:none", - "--recurse-submodules=no", - "--no-write-fetch-head", - remote, - remoteCommitReference); + buildGitCommand( + "fetch", + "--filter=blob:none", + "--recurse-submodules=no", + "--no-write-fetch-head", + remote, + remoteCommitReference)); return (Void) null; }); @@ -393,11 +439,11 @@ public CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent) commandExecutor .executeCommand( IOUtils::readFully, - "git", - "show", - commit, - "-s", - "--format=%H\",\"%an\",\"%ae\",\"%aI\",\"%cn\",\"%ce\",\"%cI\",\"%B") + buildGitCommand( + "show", + commit, + "-s", + "--format=%H\",\"%an\",\"%ae\",\"%aI\",\"%cn\",\"%ce\",\"%cI\",\"%B")) .trim(); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.error("Failed to fetch commit info", e); @@ -436,12 +482,12 @@ public List getLatestCommits() () -> commandExecutor.executeCommand( IOUtils::readLines, - "git", - "log", - "--format=%H", - "-n", - String.valueOf(latestCommitsLimit), - String.format("--since='%s'", latestCommitsSince))); + buildGitCommand( + "log", + "--format=%H", + "-n", + String.valueOf(latestCommitsLimit), + String.format("--since='%s'", latestCommitsSince)))); } /** @@ -464,23 +510,22 @@ public List getObjects( return executeCommand( Command.GET_OBJECTS, () -> { - String[] command = new String[6 + commitsToSkip.size() + commitsToInclude.size()]; - command[0] = "git"; - command[1] = "rev-list"; - command[2] = "--objects"; - command[3] = "--no-object-names"; - command[4] = "--filter=blob:none"; - command[5] = String.format("--since='%s'", latestCommitsSince); - - int count = 6; + String[] gitArgs = new String[5 + commitsToSkip.size() + commitsToInclude.size()]; + gitArgs[0] = "rev-list"; + gitArgs[1] = "--objects"; + gitArgs[2] = "--no-object-names"; + gitArgs[3] = "--filter=blob:none"; + gitArgs[4] = String.format("--since='%s'", latestCommitsSince); + + int count = 5; for (String commitToSkip : commitsToSkip) { - command[count++] = "^" + commitToSkip; + gitArgs[count++] = "^" + commitToSkip; } for (String commitToInclude : commitsToInclude) { - command[count++] = commitToInclude; + gitArgs[count++] = commitToInclude; } - return commandExecutor.executeCommand(IOUtils::readLines, command); + return commandExecutor.executeCommand(IOUtils::readLines, buildGitCommand(gitArgs)); }); } @@ -510,11 +555,7 @@ public Path createPackFiles(List objectHashes) commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, input, - "git", - "pack-objects", - "--compression=9", - "--max-pack-size=3m", - path); + buildGitCommand("pack-objects", "--compression=9", "--max-pack-size=3m", path)); return tempDirectory; }); } @@ -587,11 +628,8 @@ String getRemoteName() throws IOException, InterruptedException, TimeoutExceptio commandExecutor .executeCommand( IOUtils::readFully, - "git", - "rev-parse", - "--abbrev-ref", - "--symbolic-full-name", - "@{upstream}") + buildGitCommand( + "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}")) .trim(); int slashIdx = remote.indexOf('/'); @@ -602,7 +640,8 @@ String getRemoteName() throws IOException, InterruptedException, TimeoutExceptio // fallback to first remote if no upstream try { - List remotes = commandExecutor.executeCommand(IOUtils::readLines, "git", "remote"); + List remotes = + commandExecutor.executeCommand(IOUtils::readLines, buildGitCommand("remote")); return remotes.get(0); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.debug("Error getting remotes", e); @@ -658,11 +697,11 @@ void tryFetchingIfNotFoundLocally(String branch, String remoteName) // check if branch exists locally as a remote ref commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "show-ref", - "--verify", - "--quiet", - "refs/remotes/" + remoteName + "/" + shortBranchName); + buildGitCommand( + "show-ref", + "--verify", + "--quiet", + "refs/remotes/" + remoteName + "/" + shortBranchName)); LOGGER.debug("Branch {}/{} exists locally, skipping fetch", remoteName, shortBranchName); return; } catch (ShellCommandExecutor.ShellCommandFailedException e) { @@ -676,7 +715,8 @@ void tryFetchingIfNotFoundLocally(String branch, String remoteName) remoteHeads = commandExecutor .executeCommand( - IOUtils::readFully, "git", "ls-remote", "--heads", remoteName, shortBranchName) + IOUtils::readFully, + buildGitCommand("ls-remote", "--heads", remoteName, shortBranchName)) .trim(); } catch (ShellCommandExecutor.ShellCommandFailedException ignored) { } @@ -691,12 +731,7 @@ void tryFetchingIfNotFoundLocally(String branch, String remoteName) try { commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - "--depth", - "1", - remoteName, - shortBranchName); + buildGitCommand("fetch", "--depth", "1", remoteName, shortBranchName)); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.debug("Branch {}/{} couldn't be fetched from remote", remoteName, shortBranchName, e); } @@ -710,10 +745,8 @@ List getBaseBranchCandidates(@Nullable String defaultBranch, String remo List branches = commandExecutor.executeCommand( IOUtils::readLines, - "git", - "for-each-ref", - "--format=%(refname:short)", - "refs/remotes/" + remoteName); + buildGitCommand( + "for-each-ref", "--format=%(refname:short)", "refs/remotes/" + remoteName)); for (String branch : branches) { if (isBaseLikeBranch(branch, remoteName) || branchesEquals(branch, defaultBranch, remoteName)) { @@ -763,11 +796,11 @@ String detectDefaultBranch(String remoteName) commandExecutor .executeCommand( IOUtils::readFully, - "git", - "symbolic-ref", - "--quiet", - "--short", - "refs/remotes/" + remoteName + "/HEAD") + buildGitCommand( + "symbolic-ref", + "--quiet", + "--short", + "refs/remotes/" + remoteName + "/HEAD")) .trim(); if (Strings.isNotBlank(defaultRef)) { return removeRemotePrefix(defaultRef, remoteName); @@ -781,11 +814,8 @@ String detectDefaultBranch(String remoteName) try { commandExecutor.executeCommand( ShellCommandExecutor.OutputParser.IGNORE, - "git", - "show-ref", - "--verify", - "--quiet", - "refs/remotes/" + remoteName + "/" + branch); + buildGitCommand( + "show-ref", "--verify", "--quiet", "refs/remotes/" + remoteName + "/" + branch)); LOGGER.debug("Found fallback default branch: {}", branch); return branch; } catch (ShellCommandExecutor.ShellCommandFailedException ignored) { @@ -843,11 +873,8 @@ List computeBranchMetrics(List candidates, String sour commandExecutor .executeCommand( IOUtils::readFully, - "git", - "rev-list", - "--left-right", - "--count", - candidate + "..." + sourceBranch) + buildGitCommand( + "rev-list", "--left-right", "--count", candidate + "..." + sourceBranch)) .trim(); String[] counts = WHITESPACE_PATTERN.split(countsResult); @@ -893,7 +920,7 @@ public String getMergeBase(@Nullable String base, @Nullable String source) } try { return commandExecutor - .executeCommand(IOUtils::readFully, "git", "merge-base", base, source) + .executeCommand(IOUtils::readFully, buildGitCommand("merge-base", base, source)) .trim(); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.debug("Error calculating common ancestor for {} and {}", base, source, e); @@ -925,42 +952,21 @@ public LineDiff getGitDiff(String baseCommit, String targetCommit) () -> commandExecutor.executeCommand( GitDiffParser::parse, - "git", - "diff", - "-U0", - "--word-diff=porcelain", - "--no-prefix", - baseCommit, - targetCommit)); + buildGitCommand( + "diff", + "-U0", + "--word-diff=porcelain", + "--no-prefix", + baseCommit, + targetCommit))); } else { return executeCommand( Command.DIFF, () -> commandExecutor.executeCommand( GitDiffParser::parse, - "git", - "diff", - "-U0", - "--word-diff=porcelain", - "--no-prefix", - baseCommit)); - } - } - - private void makeRepoRootSafeDirectory() { - // Some CI envs check out the repo as a different user than the one running the command - // This will avoid the "dubious ownership" error - try { - commandExecutor.executeCommand( - ShellCommandExecutor.OutputParser.IGNORE, - "git", - "config", - "--global", - "--add", - "safe.directory", - repoRoot); - } catch (IOException | TimeoutException | InterruptedException e) { - LOGGER.debug("Failed to add safe directory", e); + buildGitCommand( + "diff", "-U0", "--word-diff=porcelain", "--no-prefix", baseCommit))); } } @@ -1009,11 +1015,8 @@ public Factory(Config config, CiVisibilityMetricCollector metricCollector) { public GitClient create(@Nullable String repoRoot) { long commandTimeoutMillis = config.getCiVisibilityGitCommandTimeoutMillis(); if (repoRoot != null && GitUtils.isValidPath(repoRoot)) { - ShellGitClient client = - new ShellGitClient( - metricCollector, repoRoot, "1 month ago", 1000, commandTimeoutMillis); - client.makeRepoRootSafeDirectory(); - return client; + return new ShellGitClient( + metricCollector, repoRoot, "1 month ago", 1000, commandTimeoutMillis); } else { LOGGER.debug("Could not determine repository root, using no-op git client"); return NoOpGitClient.INSTANCE; diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy index 479c3ce922c..8eb14cc2b0c 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy @@ -23,6 +23,59 @@ class GitClientTest extends Specification { @TempDir private Path tempDir + def "test buildGitCommand adds safe directory option with absolute path"() { + given: + def repoRoot = "/path/to/repo" + def metricCollector = Stub(CiVisibilityMetricCollectorImpl) + def gitClient = new ShellGitClient(metricCollector, repoRoot, "25 years ago", 10, GIT_COMMAND_TIMEOUT_MILLIS) + + when: + def command = gitClient.buildGitCommand("status", "--porcelain") + + then: + command.length == 5 + command[0] == "git" + command[1] == "-c" + command[2] == "safe.directory=/path/to/repo" + command[3] == "status" + command[4] == "--porcelain" + } + + def "test buildGitCommand resolves relative path to absolute"() { + given: + def repoRoot = "." + def metricCollector = Stub(CiVisibilityMetricCollectorImpl) + def gitClient = new ShellGitClient(metricCollector, repoRoot, "25 years ago", 10, GIT_COMMAND_TIMEOUT_MILLIS) + + when: + def command = gitClient.buildGitCommand("status") + + then: + command.length == 4 + command[0] == "git" + command[1] == "-c" + // The relative path "." should be resolved to an absolute path + command[2].startsWith("safe.directory=/") + !command[2].contains("safe.directory=.") + command[3] == "status" + } + + def "test buildGitCommand finds repo root from subdirectory"() { + given: + givenGitRepo() + // Create a subdirectory within the git repo + def subDir = tempDir.resolve("subdir") + Files.createDirectories(subDir) + def metricCollector = Stub(CiVisibilityMetricCollectorImpl) + def gitClient = new ShellGitClient(metricCollector, subDir.toString(), "25 years ago", 10, GIT_COMMAND_TIMEOUT_MILLIS) + + when: + def command = gitClient.buildGitCommand("status") + + then: + command[2] == "safe.directory=" + tempDir.toRealPath().toString() + } + def "test is not shallow"() { given: givenGitRepo() @@ -193,16 +246,16 @@ class GitClientTest extends Specification { then: commits == [ - "5b6f3a6dab5972d73a56dff737bd08d995255c08", - "98cd7c8e9cf71e02dc28bd9b13928bee0f85b74c", - "31ca182c0474f6265e660498c4fbcf775e23bba0", - "1bd740dd476c38d4b4d706d3ad7cb59cd0b84f7d", - "2b788c66fc4b58ce6ca7b94fbaf1b94a3ea3a93e", - "15d5d8e09cbf369f2fa6929c0b0c74b2b0a22193", - "6aaa4085c10d16b63a910043e35dbd35d2ef7f1c", - "10599ae3c17d66d642f9f143b1ff3dd236111e2a", - "5128e6f336cce5a431df68fa0ec42f8c8d0776b1", - "0c623e9dab4349960930337c936bf9975456e82f" + "5b6f3a6dab5972d73a56dff737bd08d995255c08", + "98cd7c8e9cf71e02dc28bd9b13928bee0f85b74c", + "31ca182c0474f6265e660498c4fbcf775e23bba0", + "1bd740dd476c38d4b4d706d3ad7cb59cd0b84f7d", + "2b788c66fc4b58ce6ca7b94fbaf1b94a3ea3a93e", + "15d5d8e09cbf369f2fa6929c0b0c74b2b0a22193", + "6aaa4085c10d16b63a910043e35dbd35d2ef7f1c", + "10599ae3c17d66d642f9f143b1ff3dd236111e2a", + "5128e6f336cce5a431df68fa0ec42f8c8d0776b1", + "0c623e9dab4349960930337c936bf9975456e82f" ] } @@ -216,14 +269,14 @@ class GitClientTest extends Specification { then: objects == [ - "5b6f3a6dab5972d73a56dff737bd08d995255c08", - "c52914110869ff3999bca4837410511f17787e87", - "cd3407343e846f6707d34b77f38e86345063d0bf", - "e7fff9f77d05daca86a6bbec334a3304da23278b", - "a70ad1f15bda97e2f154a0ac6577e11d55ee05d3", - "3d02ff4958a9ef00b36b1f6e755e3e4e9c92ba5f", - "5ba3615fbe9ae3dd4338fae6f67f013c212f83b5", - "fd408d6995f1651a245c227d57529bf8a51ffe45" + "5b6f3a6dab5972d73a56dff737bd08d995255c08", + "c52914110869ff3999bca4837410511f17787e87", + "cd3407343e846f6707d34b77f38e86345063d0bf", + "e7fff9f77d05daca86a6bbec334a3304da23278b", + "a70ad1f15bda97e2f154a0ac6577e11d55ee05d3", + "3d02ff4958a9ef00b36b1f6e755e3e4e9c92ba5f", + "5ba3615fbe9ae3dd4338fae6f67f013c212f83b5", + "fd408d6995f1651a245c227d57529bf8a51ffe45" ] } @@ -234,14 +287,14 @@ class GitClientTest extends Specification { when: def gitClient = givenGitClient() def packFilesDir = gitClient.createPackFiles([ - "5b6f3a6dab5972d73a56dff737bd08d995255c08", - "c52914110869ff3999bca4837410511f17787e87", - "cd3407343e846f6707d34b77f38e86345063d0bf", - "e7fff9f77d05daca86a6bbec334a3304da23278b", - "a70ad1f15bda97e2f154a0ac6577e11d55ee05d3", - "3d02ff4958a9ef00b36b1f6e755e3e4e9c92ba5f", - "5ba3615fbe9ae3dd4338fae6f67f013c212f83b5", - "fd408d6995f1651a245c227d57529bf8a51ffe45" + "5b6f3a6dab5972d73a56dff737bd08d995255c08", + "c52914110869ff3999bca4837410511f17787e87", + "cd3407343e846f6707d34b77f38e86345063d0bf", + "e7fff9f77d05daca86a6bbec334a3304da23278b", + "a70ad1f15bda97e2f154a0ac6577e11d55ee05d3", + "3d02ff4958a9ef00b36b1f6e755e3e4e9c92ba5f", + "5ba3615fbe9ae3dd4338fae6f67f013c212f83b5", + "fd408d6995f1651a245c227d57529bf8a51ffe45" ]) then: @@ -265,7 +318,7 @@ class GitClientTest extends Specification { then: diff.linesByRelativePath == [ - "src/Datadog.Trace/Logging/DatadogLogging.cs": lines(26, 32, 91, 95, 159, 160) + "src/Datadog.Trace/Logging/DatadogLogging.cs": lines(26, 32, 91, 95, 159, 160) ] } @@ -397,16 +450,16 @@ class GitClientTest extends Specification { sortedBranches == expectedOrder where: - metrics | expectedOrder + metrics | expectedOrder [ - new ShellGitClient.BaseBranchMetric("main", 10, 2), - new ShellGitClient.BaseBranchMetric("master", 15, 1), - new ShellGitClient.BaseBranchMetric("origin/main", 5, 2)] | ["master", "main", "origin/main"] + new ShellGitClient.BaseBranchMetric("main", 10, 2), + new ShellGitClient.BaseBranchMetric("master", 15, 1), + new ShellGitClient.BaseBranchMetric("origin/main", 5, 2)] | ["master", "main", "origin/main"] [ - new ShellGitClient.BaseBranchMetric("main", 10, 2), - new ShellGitClient.BaseBranchMetric("master", 15, 2), - new ShellGitClient.BaseBranchMetric("origin/main", 5, 2)] | ["main", "origin/main", "master"] - [] | [] + new ShellGitClient.BaseBranchMetric("main", 10, 2), + new ShellGitClient.BaseBranchMetric("master", 15, 2), + new ShellGitClient.BaseBranchMetric("origin/main", 5, 2)] | ["main", "origin/main", "master"] + [] | [] } def "test get base branch sha: #testcaseName"() {