This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch test/133-local-git-fixtures in repository https://gitbox.apache.org/repos/asf/camel-kit.git
commit 7decf98514db10ee1bc149655672d85931b6b27b Author: Luigi De Masi <[email protected]> AuthorDate: Wed Oct 28 19:45:37 2026 +0100 test: cover GitRepoFetcher with local Git fixtures Replace disabled GitHub-dependent tests with temporary repositories served by JGit on loopback. Cover shallow clones, file discovery, incremental updates, tag caching, failure recovery and explicit refresh in normal CI. Fixes #133 Co-authored-by: OpenAI Codex <[email protected]> --- .../kit/knowledge/indexer/GitRepoFetcherTest.java | 248 +++++++++++++++++++-- 1 file changed, 229 insertions(+), 19 deletions(-) diff --git a/knowledge/indexer/src/test/java/org/apache/camel/kit/knowledge/indexer/GitRepoFetcherTest.java b/knowledge/indexer/src/test/java/org/apache/camel/kit/knowledge/indexer/GitRepoFetcherTest.java index 5ddb203f..9d8043d9 100644 --- a/knowledge/indexer/src/test/java/org/apache/camel/kit/knowledge/indexer/GitRepoFetcherTest.java +++ b/knowledge/indexer/src/test/java/org/apache/camel/kit/knowledge/indexer/GitRepoFetcherTest.java @@ -1,44 +1,254 @@ package org.apache.camel.kit.knowledge.indexer; +import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import org.junit.jupiter.api.Disabled; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.Daemon; +import org.eclipse.jgit.transport.resolver.FileResolver; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; -@Disabled("Requires network access to GitHub") +@Timeout(30) class GitRepoFetcherTest { + private static final PersonIdent IDENTITY = new PersonIdent("Test fixture", "[email protected]"); + @TempDir Path tempDir; + private Git upstream; + private Daemon daemon; + private String repoUrl; + private GitRepoFetcher fetcher; + private RevCommit latest; + + @BeforeEach + void createLocalRepository() throws Exception { + upstream = Git.init().setDirectory(tempDir.resolve("upstream").toFile()).setInitialBranch("main").call(); + var config = upstream.getRepository().getConfig(); + config.setBoolean("commit", null, "gpgSign", false); + config.setBoolean("tag", null, "gpgSign", false); + config.setBoolean("tag", null, "forceSignAnnotated", false); + config.save(); + writeUpstream("document.md", "first version"); + writeUpstream("notes.txt", "not a Markdown document"); + writeUpstream("nested/other.md", "nested document"); + commit("first version"); + tag("v1"); + writeUpstream("document.md", "second version"); + latest = commit("second version"); + tag("v2"); + + // The local file transport cannot exercise the shallow-clone protocol. + daemon = new Daemon(new InetSocketAddress("127.0.0.1", 0)); + daemon.setRepositoryResolver(new FileResolver<>(tempDir.toFile(), true)); + daemon.setTimeout(10); + daemon.start(); + repoUrl = "git://127.0.0.1:" + daemon.getAddress().getPort() + "/upstream"; + fetcher = new GitRepoFetcher(tempDir.resolve("cache")); + } + + @AfterEach + void closeLocalRepository() throws Exception { + try { + if (daemon != null) { + daemon.stopAndWait(); + } + } finally { + if (upstream != null) { + upstream.close(); + } + } + } + + @Test + void clonesBranchWithDepthOne() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "main", "documents"); + + assertEquals(tempDir.resolve("cache/documents"), repoDir); + assertEquals("second version", Files.readString(repoDir.resolve("document.md"))); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(latest.getId(), clone.getRepository().resolve("HEAD")); + assertTrue(clone.getRepository().getObjectDatabase().getShallowCommits().contains(latest.getId())); + var history = clone.log().call().iterator(); + assertEquals(latest.getId(), history.next().getId()); + assertFalse(history.hasNext(), "The clone must not contain the first commit's history"); + } + } + + @Test + void listsOnlyMatchingDocumentFiles() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "main", "documents"); + Files.createDirectory(repoDir.resolve("directory.md")); + + assertEquals(List.of(repoDir.resolve("document.md")), fetcher.listFiles(repoDir, "*.md")); + assertEquals(List.of(repoDir.resolve("document.md"), repoDir.resolve("nested/other.md")), + fetcher.listFilesRecursive(repoDir, "*.md").stream().sorted().toList()); + assertEquals("nested document", Files.readString(repoDir.resolve("nested/other.md"))); + assertTrue(fetcher.listFiles(repoDir.resolve("missing"), "*.md").isEmpty()); + assertTrue(fetcher.listFilesRecursive(repoDir.resolve("missing"), "*.md").isEmpty()); + } + + @Test + void updatesExistingCloneAndResetsTrackedContents() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "main", "documents", false); + Path sentinel = repoDir.resolve(".git/retained-on-update"); + Files.writeString(sentinel, "original repository"); + Files.writeString(repoDir.resolve("document.md"), "local changes"); + Files.delete(repoDir.resolve("nested/other.md")); + writeUpstream("document.md", "third version"); + writeUpstream("added.md", "new upstream document"); + RevCommit next = commit("third version"); + + assertEquals(repoDir, fetcher.fetchRepo(repoUrl, "main", "documents", false)); + + assertEquals("third version", Files.readString(repoDir.resolve("document.md"))); + assertEquals("nested document", Files.readString(repoDir.resolve("nested/other.md"))); + assertEquals("new upstream document", Files.readString(repoDir.resolve("added.md"))); + assertEquals("original repository", Files.readString(sentinel), "An update must not replace the clone"); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(next.getId(), clone.getRepository().resolve("HEAD")); + assertTrue(clone.status().call().isClean()); + } + } + + @Test + void listsAnnotatedTagsWithoutPeeledDuplicates() throws Exception { + assertEquals(List.of("v1", "v2"), GitRepoFetcher.listRemoteTags(repoUrl).stream().sorted().toList()); + } + + @Test + void checksOutRequestedTag() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "v1", "documents", true); + + assertEquals("first version", Files.readString(repoDir.resolve("document.md"))); + assertEquals("v1", Files.readString(repoDir.resolve(".fetched-tag"))); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(upstream.getRepository().resolve("v1^{commit}"), clone.getRepository().resolve("HEAD")); + } + } + @Test - void clonesRepoAndListsFiles() throws Exception { - GitRepoFetcher fetcher = new GitRepoFetcher(tempDir); + void reusesUnchangedTagWithoutRemoteAccess() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "v1", "documents", true); + Path sentinel = repoDir.resolve(".git/retained-on-cache-hit"); + Files.writeString(sentinel, "cached repository"); + daemon.stopAndWait(); + assertFalse(daemon.isRunning()); - Path repoDir = fetcher.fetchRepo( - "https://github.com/apache/camel-website.git", - "main", - "camel-website"); + assertEquals(repoDir, fetcher.fetchRepo(repoUrl, "v1", "documents", true)); - assertTrue(Files.isDirectory(repoDir)); - assertTrue(Files.isDirectory(repoDir.resolve("content/security"))); + assertEquals("first version", Files.readString(repoDir.resolve("document.md"))); + assertEquals("cached repository", Files.readString(sentinel)); } @Test - void listFilesWithPattern() throws Exception { - GitRepoFetcher fetcher = new GitRepoFetcher(tempDir); - Path repoDir = fetcher.fetchRepo( - "https://github.com/apache/camel-website.git", - "main", - "camel-website"); + void replacesCloneWhenRequestedTagChanges() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "v1", "documents", true); + Path sentinel = repoDir.resolve(".git/replaced-on-tag-change"); + Files.writeString(sentinel, "old tag"); + + assertEquals(repoDir, fetcher.fetchRepo(repoUrl, "v2", "documents", true)); + + assertEquals("second version", Files.readString(repoDir.resolve("document.md"))); + assertEquals("v2", Files.readString(repoDir.resolve(".fetched-tag"))); + assertFalse(Files.exists(sentinel)); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(latest.getId(), clone.getRepository().resolve("HEAD")); + } + } + + @Test + void recoversIncompleteClone() throws Exception { + Path repoDir = tempDir.resolve("cache/documents"); + Files.createDirectories(repoDir.resolve("partial")); + Path partial = repoDir.resolve("partial/download.md"); + Files.writeString(partial, "interrupted clone"); + + assertEquals(repoDir, fetcher.fetchRepo(repoUrl, "main", "documents")); + + assertTrue(Files.isDirectory(repoDir.resolve(".git"))); + assertEquals("second version", Files.readString(repoDir.resolve("document.md"))); + assertFalse(Files.exists(partial)); + } + + @Test + void reclonesFromSuppliedUrlAfterUpdateFails() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "main", "documents"); + Path sentinel = repoDir.resolve(".git/replaced-after-failure"); + Files.writeString(sentinel, "original repository"); + try (Git clone = Git.open(repoDir.toFile())) { + var config = clone.getRepository().getConfig(); + config.setString("remote", "origin", "url", repoUrl + "-missing"); + config.save(); + } + writeUpstream("document.md", "third version"); + RevCommit next = commit("third version"); + + assertEquals(repoDir, fetcher.fetchRepo(repoUrl, "main", "documents")); + + assertEquals("third version", Files.readString(repoDir.resolve("document.md"))); + assertFalse(Files.exists(sentinel)); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(next.getId(), clone.getRepository().resolve("HEAD")); + assertEquals(repoUrl, clone.getRepository().getConfig().getString("remote", "origin", "url")); + } + } + + @Test + void removesFailedCloneAndReportsRequestedRepositoryAndRef() { + IOException failure = assertThrows(IOException.class, + () -> fetcher.fetchRepo(repoUrl, "missing-branch", "documents")); + + assertTrue(failure.getMessage().contains("Failed to clone " + repoUrl + " at missing-branch")); + assertInstanceOf(GitAPIException.class, failure.getCause()); + assertFalse(Files.exists(tempDir.resolve("cache/documents"))); + } + + @Test + void explicitRefreshReplacesExistingClone() throws Exception { + Path repoDir = fetcher.fetchRepo(repoUrl, "main", "documents"); + Path sentinel = repoDir.resolve(".git/replaced-on-refresh"); + Files.writeString(sentinel, "original repository"); + Files.writeString(repoDir.resolve("untracked.md"), "remove during refresh"); + writeUpstream("document.md", "third version"); + RevCommit next = commit("third version"); + + assertEquals(repoDir, fetcher.refreshRepo(repoUrl, "main", "documents")); + + assertEquals("third version", Files.readString(repoDir.resolve("document.md"))); + assertFalse(Files.exists(sentinel)); + assertFalse(Files.exists(repoDir.resolve("untracked.md"))); + try (Git clone = Git.open(repoDir.toFile())) { + assertEquals(next.getId(), clone.getRepository().resolve("HEAD")); + } + } + + private void writeUpstream(String relativePath, String content) throws Exception { + Path path = upstream.getRepository().getWorkTree().toPath().resolve(relativePath); + Files.createDirectories(path.getParent()); + Files.writeString(path, content); + } + + private RevCommit commit(String message) throws Exception { + upstream.add().addFilepattern(".").call(); + return upstream.commit().setMessage(message).setAuthor(IDENTITY).setCommitter(IDENTITY) + .setSign(false).setNoVerify(true).call(); + } - List<Path> cveFiles = fetcher.listFiles(repoDir.resolve("content/security"), "CVE-*.md"); - assertFalse(cveFiles.isEmpty(), "Should find CVE advisory files"); - assertTrue(cveFiles.stream().allMatch(p -> p.getFileName().toString().startsWith("CVE-"))); + private void tag(String name) throws Exception { + upstream.tag().setName(name).setAnnotated(true).setMessage(name).setTagger(IDENTITY).setSigned(false).call(); } }
