Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private Path getGitPath(String repositoryPath) {

@Override
public int order() {
return 2;
return 3;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public GitInfo build(@Nullable String repositoryPath) {

@Override
public int order() {
return 1;
return 2;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package datadog.trace.civisibility.git;

import static java.util.Collections.emptyList;

import datadog.trace.api.Config;
import datadog.trace.api.civisibility.telemetry.tag.GitProviderDiscrepant;
import datadog.trace.api.civisibility.telemetry.tag.GitProviderExpected;
import datadog.trace.api.git.CommitInfo;
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.GitInfoBuilder;
import datadog.trace.civisibility.git.tree.GitClient;
import datadog.trace.civisibility.utils.ShellCommandExecutor;
import java.util.List;
import javax.annotation.Nullable;
import org.slf4j.Logger;
Expand All @@ -33,14 +36,23 @@ public GitInfo build(@Nullable String repositoryPath) {
GitClient gitClient = gitClientFactory.create(repositoryPath);
try {
String remoteName = config.getCiVisibilityGitRemoteName();
String remoteUrl = gitClient.getRemoteUrl(remoteName);
String remoteUrl = null;
try {
remoteUrl = gitClient.getRemoteUrl(remoteName);
} catch (ShellCommandExecutor.ShellCommandFailedException e) {
LOGGER.debug("Could not read Git remote URL from {}", repositoryPath, e);
Comment thread
juan-fernandez marked this conversation as resolved.
Resolved
}
String branch = gitClient.getCurrentBranch();
List<String> tags = gitClient.getTags(GitClient.HEAD);
String tag = !tags.isEmpty() ? tags.iterator().next() : null;
List<String> tags =
branch == null || branch.isEmpty() ? gitClient.getTags(GitClient.HEAD) : emptyList();
String tag = tags.size() == 1 ? tags.get(0) : null;
Comment thread
juan-fernandez marked this conversation as resolved.
Resolved

CommitInfo commitInfo = gitClient.getCommitInfo(GitClient.HEAD, false);
return new GitInfo(remoteUrl, branch, tag, commitInfo);

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return GitInfo.NOOP;
} catch (Exception e) {
LOGGER.debug("Error while getting Git data from {}", repositoryPath, e);
LOGGER.warn("Error while getting Git data by executing shell commands");
Expand All @@ -50,7 +62,7 @@ public GitInfo build(@Nullable String repositoryPath) {

@Override
public int order() {
return 3;
return 1;
Comment thread
juan-fernandez marked this conversation as resolved.
Resolved
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent)
@Nonnull
List<String> getLatestCommits() throws IOException, TimeoutException, InterruptedException;

@Nonnull
List<String> getLatestCommits(String commit)
throws IOException, TimeoutException, InterruptedException;

@Nonnull
List<String> getObjects(Collection<String> commitsToSkip, Collection<String> commitsToInclude)
throws IOException, TimeoutException, InterruptedException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ private void uploadGitData() {

GitInfo gitInfo = gitInfoProvider.getGitInfo(repoRoot);
String remoteUrl = gitInfo.getRepositoryURL();
List<String> latestCommits = gitClient.getLatestCommits();
String selectedCommit = gitInfo.getCommit().getSha();
List<String> latestCommits = gitClient.getLatestCommits(selectedCommit);
Comment thread
juan-fernandez marked this conversation as resolved.
Resolved
boolean unshallowed = false;
if (latestCommits.isEmpty() && config.isCiVisibilityGitUnshallowDefer()) {
unshallowed = gitRepoUnshallow.unshallow();
if (unshallowed) {
latestCommits = gitClient.getLatestCommits(selectedCommit);
Comment on lines +102 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fetch the selected revision before giving up

When an explicit DD_GIT_COMMIT_SHA names a revision absent from a shallow clone—particularly an older commit outside latestCommitsSince or a commit on another branch—this retry still cannot recover it: GitRepoUnshallow.unshallow() fetches only HEAD/upstream, and ShellGitClient.unshallow() applies --shallow-since, so getLatestCommits(selectedCommit) remains empty and the upload is silently skipped. The new empty-history retry fixes the earlier control-flow failure, but the selected revision itself must be fetched before concluding that there is no data to upload.

Useful? React with 👍 / 👎.

}
}
if (latestCommits.isEmpty()) {
LOGGER.debug("No commits in the last month, skipping git data upload");
callback.complete(null);
Expand All @@ -112,8 +120,10 @@ private void uploadGitData() {
return;
}

if (config.isCiVisibilityGitUnshallowDefer() && gitRepoUnshallow.unshallow()) {
latestCommits = gitClient.getLatestCommits();
if (config.isCiVisibilityGitUnshallowDefer()
&& !unshallowed
&& gitRepoUnshallow.unshallow()) {
latestCommits = gitClient.getLatestCommits(selectedCommit);
commitsToSkip = gitDataApi.searchCommits(remoteUrl, latestCommits);
}

Expand All @@ -132,7 +142,7 @@ private void uploadGitData() {
return;
}

String currentCommit = latestCommits.get(0);
String currentCommit = gitInfo.getCommit().getSha();

Path packFilesDirectory = gitClient.createPackFiles(objectHashes);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent) {
}

@Nonnull
@Override
public List<String> getLatestCommits(String commit) {
return Collections.emptyList();
}

@Override
public List<String> getLatestCommits() {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
Expand All @@ -45,7 +46,7 @@ public class ShellGitClient implements GitClient {
Arrays.asList("release/", "hotfix/");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final String ORIGIN = "origin";
private static final Pattern COMMIT_INFO_SPLIT = Pattern.compile("\",\"");
private static final Pattern COMMIT_INFO_SPLIT = Pattern.compile("\\x00");

private final CiVisibilityMetricCollector metricCollector;
private final String repoRoot;
Expand Down Expand Up @@ -323,10 +324,9 @@ public List<String> getTags(String commit)
() -> {
try {
return commandExecutor.executeCommand(
IOUtils::readLines, buildGitCommand("describe", "--tags", "--exact-match", commit));
IOUtils::readLines, buildGitCommand("tag", "--no-column", "--points-at", commit));
} catch (ShellCommandExecutor.ShellCommandFailedException e) {
// if provided commit is not tagged,
// command will fail because "--exact-match" is specified
// An unavailable revision cannot have local tags.
return Collections.emptyList();
}
});
Expand Down Expand Up @@ -438,14 +438,15 @@ public CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent)
"show",
commit,
"-s",
"--format=%H\",\"%an\",\"%ae\",\"%aI\",\"%cn\",\"%ce\",\"%cI\",\"%B"))
"--no-show-signature",
"--format=%H%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%B"))
.trim();
} catch (ShellCommandExecutor.ShellCommandFailedException e) {
LOGGER.error("Failed to fetch commit info", e);
return CommitInfo.NOOP;
}

String[] fields = COMMIT_INFO_SPLIT.split(info);
String[] fields = COMMIT_INFO_SPLIT.split(info, 8);
if (fields.length < 8) {
LOGGER.error("Could not parse commit info: {}", info);
return CommitInfo.NOOP;
Expand All @@ -472,17 +473,42 @@ public CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent)
@Override
public List<String> getLatestCommits()
throws IOException, TimeoutException, InterruptedException {
return executeCommand(
Command.GET_LOCAL_COMMITS,
() ->
commandExecutor.executeCommand(
IOUtils::readLines,
buildGitCommand(
"log",
"--format=%H",
"-n",
String.valueOf(latestCommitsLimit),
String.format("--since='%s'", latestCommitsSince))));
return getLatestCommits(HEAD);
}

@Nonnull
@Override
public List<String> getLatestCommits(String commit)
throws IOException, TimeoutException, InterruptedException {
if (GitUtils.isNotValidCommit(commit)) {
return Collections.emptyList();
}
List<String> commits;
try {
commits =
executeCommand(
Command.GET_LOCAL_COMMITS,
() ->
commandExecutor.executeCommand(
IOUtils::readLines,
buildGitCommand(
"log",
"--format=%H",
"-n",
String.valueOf(latestCommitsLimit),
String.format("--since='%s'", latestCommitsSince),
commit,
"--")));
} catch (ShellCommandExecutor.ShellCommandFailedException e) {
if (!isCommitPresent(commit)) {
return Collections.emptyList();
}
throw e;
}
if (!HEAD.equals(commit) && !commits.contains(commit) && isCommitPresent(commit)) {
commits.add(0, commit);
}
return commits;
}

/**
Expand All @@ -505,22 +531,45 @@ public List<String> getObjects(
return executeCommand(
Command.GET_OBJECTS,
() -> {
String[] gitArgs = new String[5 + commitsToSkip.size() + commitsToInclude.size()];
String[] gitArgs = new String[6 + 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);
gitArgs[5] = "--max-count=" + latestCommitsLimit;

int count = 5;
int count = 6;
for (String commitToSkip : commitsToSkip) {
gitArgs[count++] = "^" + commitToSkip;
}
for (String commitToInclude : commitsToInclude) {
gitArgs[count++] = commitToInclude;
}

return commandExecutor.executeCommand(IOUtils::readLines, buildGitCommand(gitArgs));
List<String> objects =
commandExecutor.executeCommand(IOUtils::readLines, buildGitCommand(gitArgs));
LinkedHashSet<String> outsideWindow = new LinkedHashSet<>(commitsToInclude);
for (String object : objects) {
outsideWindow.remove(object);
}
Comment on lines +552 to +555

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not re-add commits excluded by known descendants

When the backend knows a newer commit but not every ancestor returned in latestCommits, the first rev-list correctly excludes that commit's reachable history via ^commitToSkip. However, every included root omitted from that result is classified as outsideWindow and then re-added with --no-walk, even when it was omitted because it is reachable from the skipped descendant rather than because of the age/count window. This defeats commitsToSkip and can package up to the full history limit of commit and tree objects that the exclusion deliberately avoided; only roots actually outside the configured window should be added separately.

Useful? React with 👍 / 👎.

if (!outsideWindow.isEmpty()) {
List<String> selectedArgs = new ArrayList<>();
Collections.addAll(
selectedArgs,
"rev-list",
"--objects",
"--no-object-names",
"--filter=blob:none",
"--no-walk=unsorted");
selectedArgs.addAll(outsideWindow);
LinkedHashSet<String> combined = new LinkedHashSet<>(objects);
combined.addAll(
commandExecutor.executeCommand(
IOUtils::readLines, buildGitCommand(selectedArgs.toArray(new String[0]))));
return new ArrayList<>(combined);
}
return objects;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ class GitClientGitInfoBuilderTest extends Specification {
"* Changes based on the review."
}

def "keeps checkout metadata when no remote is configured"() {
given:
givenGitRepo()
Files.delete(tempDir.resolve(".git/config"))
def metricCollector = Stub(CiVisibilityMetricCollectorImpl)
def config = Stub(Config) {
getCiVisibilityGitRemoteName() >> "origin"
getCiVisibilityGitCommandTimeoutMillis() >> GIT_COMMAND_TIMEOUT_MILLIS
}
def builder = new GitClientGitInfoBuilder(config, new ShellGitClient.Factory(config, metricCollector))

when:
def gitInfo = builder.build(tempDir.toString())

then:
gitInfo.repositoryURL == null
gitInfo.branch == "master"
gitInfo.commit.sha == "5b6f3a6dab5972d73a56dff737bd08d995255c08"
gitInfo.commit.author.name == "Tony Redondo"
}

private void givenGitRepo() {
givenGitRepo("ci/git/with_pack/git")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package datadog.trace.civisibility.git.tree

import datadog.trace.api.Config
import datadog.trace.civisibility.telemetry.CiVisibilityMetricCollectorImpl
import datadog.trace.api.git.CommitInfo
import datadog.trace.api.git.GitInfo
import datadog.trace.api.git.GitInfoProvider
import datadog.communication.util.IOUtils
Expand Down Expand Up @@ -37,7 +38,7 @@ class GitDataUploaderImplTest extends Specification {
}
def api = Mock(GitDataApi)
def gitInfoProvider = Stub(GitInfoProvider)
gitInfoProvider.getGitInfo(repoRoot) >> new GitInfo(repoUrl, null, null, null)
gitInfoProvider.getGitInfo(repoRoot) >> new GitInfo(repoUrl, null, null, new CommitInfo("5b6f3a6dab5972d73a56dff737bd08d995255c08"))

def gitClient = new ShellGitClient(metricCollector, repoRoot, "25 years ago", 3, TIMEOUT_MILLIS)
def unshallow = new GitRepoUnshallow(config, gitClient)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package datadog.trace.civisibility.git;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import datadog.trace.api.Config;
import datadog.trace.api.git.CommitInfo;
import datadog.trace.api.git.GitInfo;
import datadog.trace.civisibility.git.tree.GitClient;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.api.Test;

class GitClientGitInfoBuilderUnitTest {
@Test
void interruptedRemoteLookupAbortsAndPreservesInterruption() throws Exception {
GitClient client = mock(GitClient.class);
when(client.getRemoteUrl("origin")).thenThrow(new InterruptedException());
try {
assertEquals(GitInfo.NOOP, builder(client).build("/repo"));
assertTrue(Thread.currentThread().isInterrupted());
verify(client, never()).getCurrentBranch();
} finally {
Thread.interrupted();
}
assertFalse(Thread.currentThread().isInterrupted());
}

@Test
void timedOutRemoteLookupAborts() throws Exception {
GitClient client = mock(GitClient.class);
when(client.getRemoteUrl("origin")).thenThrow(new TimeoutException());
assertEquals(GitInfo.NOOP, builder(client).build("/repo"));
verify(client, never()).getCurrentBranch();
}

@Test
void tagsAreOnlyInferredForAnUnambiguousDetachedCheckout() throws Exception {
GitClient client = mock(GitClient.class);
when(client.getCommitInfo(GitClient.HEAD, false)).thenReturn(new CommitInfo("local-sha"));
when(client.getCurrentBranch()).thenReturn("main", "", "");
when(client.getTags(GitClient.HEAD)).thenReturn(singletonList("v1"), asList("v1", "stable"));
GitClientGitInfoBuilder builder = builder(client);
assertNull(builder.build("/repo").getTag());
assertEquals("v1", builder.build("/repo").getTag());
assertNull(builder.build("/repo").getTag());
}

private GitClientGitInfoBuilder builder(GitClient client) {
Config config = mock(Config.class);
when(config.getCiVisibilityGitRemoteName()).thenReturn("origin");
GitClient.Factory factory = mock(GitClient.Factory.class);
when(factory.create("/repo")).thenReturn(client);
return new GitClientGitInfoBuilder(config, factory);
}
}
Loading
Loading