GitScmProvider extends ScmProvider { @CheckForNull @Override public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) { try (Repository repo = buildRepo(rootBaseDir)) { Ref targetRef = resolveTargetRef(targetBranchName, repo); if (targetRef == null) { analysisWarnings.addUnique(String.format("Could not find ref '%s' in refs/heads, refs/remotes/upstream or refs/remotes/origin. " + "You may see unexpected issues and changes. " + "Please make sure to fetch this ref before pull request analysis.", targetBranchName)); return null; } if (isDiffAlgoInvalid(repo.getConfig())) { LOG.warn("The diff algorithm configured in git is not supported. " + "No information regarding changes in the branch will be collected, which can lead to unexpected results."); return null; } Optional<RevCommit> mergeBaseCommit = findMergeBase(repo, targetRef); if (!mergeBaseCommit.isPresent()) { LOG.warn("No merge base found between HEAD and " + targetRef.getName()); return null; } AbstractTreeIterator mergeBaseTree = prepareTreeParser(repo, mergeBaseCommit.get()); try (Git git = newGit(repo)) { List<DiffEntry> diffEntries = git.diff() .setShowNameAndStatusOnly(true) .setOldTree(mergeBaseTree) .setNewTree(prepareNewTree(repo)) .call(); return diffEntries.stream() .filter(diffEntry -> diffEntry.getChangeType() == DiffEntry.ChangeType.ADD || diffEntry.getChangeType() == DiffEntry.ChangeType.MODIFY) .map(diffEntry -> repo.getWorkTree().toPath().resolve(diffEntry.getNewPath())) .collect(Collectors.toSet()); } } catch (IOException | GitAPIException e) { LOG.warn(e.getMessage(), e); } return null; } GitScmProvider(JGitBlameCommand jgitBlameCommand, AnalysisWarnings analysisWarnings, GitIgnoreCommand gitIgnoreCommand, System2 system2); @Override GitIgnoreCommand ignoreCommand(); @Override String key(); @Override boolean supports(File baseDir); @Override BlameCommand blameCommand(); @CheckForNull @Override Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir); @CheckForNull @Override Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path projectBaseDir, Set<Path> changedFiles); @CheckForNull Instant forkDate(String referenceBranchName, Path projectBaseDir); @Override Path relativePathFromScmRoot(Path path); @Override @CheckForNull String revisionId(Path path);  }
@Test public void branchChangedFiles_when_git_work_tree_is_above_project_basedir() throws IOException, GitAPIException { git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); Path projectDir = worktree.resolve("project"); Files.createDirectory(projectDir); createAndCommitFile("project/file-b1"); assertThat(newScmProvider().branchChangedFiles("master", projectDir)) .containsOnly(projectDir.resolve("file-b1")); }
@Test public void branchChangedFiles_falls_back_to_origin_when_local_branch_does_not_exist() throws IOException, GitAPIException { git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); createAndCommitFile("file-b1"); Path worktree2 = temp.newFolder().toPath(); Git.cloneRepository() .setURI(worktree.toString()) .setDirectory(worktree2.toFile()) .call(); assertThat(newScmProvider().branchChangedFiles("master", worktree2)) .containsOnly(worktree2.resolve("file-b1")); verifyZeroInteractions(analysisWarnings); }
@Test public void branchChangedFiles_use_remote_target_ref_when_running_on_circle_ci() throws IOException, GitAPIException { when(system2.envVariable("CIRCLECI")).thenReturn("true"); git.checkout().setName("b1").setCreateBranch(true).call(); createAndCommitFile("file-b1"); Path worktree2 = temp.newFolder().toPath(); Git local = Git.cloneRepository() .setURI(worktree.toString()) .setDirectory(worktree2.toFile()) .call(); local.checkout().setCreateBranch(true).setName("master").setStartPoint("origin/b1").call(); local.checkout().setName("b1").call(); assertThat(newScmProvider().branchChangedFiles("master", worktree2)) .containsOnly(worktree2.resolve("file-b1")); verifyZeroInteractions(analysisWarnings); }
@Test public void branchChangedFiles_falls_back_to_local_ref_if_origin_branch_does_not_exist_when_running_on_circle_ci() throws IOException, GitAPIException { when(system2.envVariable("CIRCLECI")).thenReturn("true"); git.checkout().setName("b1").setCreateBranch(true).call(); createAndCommitFile("file-b1"); Path worktree2 = temp.newFolder().toPath(); Git local = Git.cloneRepository() .setURI(worktree.toString()) .setDirectory(worktree2.toFile()) .call(); local.checkout().setName("local-only").setCreateBranch(true).setStartPoint("origin/master").call(); local.checkout().setName("b1").call(); assertThat(newScmProvider().branchChangedFiles("local-only", worktree2)) .containsOnly(worktree2.resolve("file-b1")); verifyZeroInteractions(analysisWarnings); }
@Test public void branchChangedFiles_falls_back_to_upstream_ref() throws IOException, GitAPIException { git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); createAndCommitFile("file-b1"); Path worktree2 = temp.newFolder().toPath(); Git.cloneRepository() .setURI(worktree.toString()) .setRemote("upstream") .setDirectory(worktree2.toFile()) .call(); assertThat(newScmProvider().branchChangedFiles("master", worktree2)) .containsOnly(worktree2.resolve("file-b1")); verifyZeroInteractions(analysisWarnings); }
@Test public void branchChangedFiles_should_return_null_when_branch_nonexistent() { assertThat(newScmProvider().branchChangedFiles("nonexistent", worktree)).isNull(); }
@Test public void branchChangedFiles_should_throw_when_repo_nonexistent() throws IOException { thrown.expect(MessageException.class); thrown.expectMessage("Not inside a Git work tree: "); newScmProvider().branchChangedFiles("master", temp.newFolder().toPath()); }
@Test public void branchChangedFiles_should_throw_when_dir_nonexistent() { thrown.expect(MessageException.class); thrown.expectMessage("Not inside a Git work tree: "); newScmProvider().branchChangedFiles("master", temp.getRoot().toPath().resolve("nonexistent")); }
@Test public void branchChangedFiles_should_return_null_on_io_errors_of_repo_builder() { GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { @Override Repository buildRepo(Path basedir) throws IOException { throw new IOException(); } }; assertThat(provider.branchChangedFiles("branch", worktree)).isNull(); verifyZeroInteractions(analysisWarnings); }
@Test public void branchChangedFiles_should_return_null_if_repo_exactref_is_null() throws IOException { Repository repository = mock(Repository.class); RefDatabase refDatabase = mock(RefDatabase.class); when(repository.getRefDatabase()).thenReturn(refDatabase); when(refDatabase.findRef("branch")).thenReturn(null); GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { @Override Repository buildRepo(Path basedir) { return repository; } }; assertThat(provider.branchChangedFiles("branch", worktree)).isNull(); String warning = "Could not find ref 'branch' in refs/heads, refs/remotes/upstream or refs/remotes/origin." + " You may see unexpected issues and changes. Please make sure to fetch this ref before pull request analysis."; verify(analysisWarnings).addUnique(warning); }
@Test public void branchChangedFiles_should_return_null_on_errors() throws GitAPIException { DiffCommand diffCommand = mock(DiffCommand.class); when(diffCommand.setShowNameAndStatusOnly(anyBoolean())).thenReturn(diffCommand); when(diffCommand.setOldTree(any())).thenReturn(diffCommand); when(diffCommand.setNewTree(any())).thenReturn(diffCommand); when(diffCommand.call()).thenThrow(mock(GitAPIException.class)); Git git = mock(Git.class); when(git.diff()).thenReturn(diffCommand); GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { @Override Git newGit(Repository repo) { return git; } }; assertThat(provider.branchChangedFiles("master", worktree)).isNull(); verify(diffCommand).call(); }
@Test public void branchChangedFiles_should_not_crash_if_branches_have_no_common_ancestors() throws GitAPIException, IOException { String fileName = "file-in-first-commit.xoo"; String renamedName = "file-renamed.xoo"; git.checkout().setOrphan(true).setName("b1").call(); Path file = worktree.resolve(fileName); Path renamed = file.resolveSibling(renamedName); addLineToFile(fileName, 1); Files.move(file, renamed); git.rm().addFilepattern(fileName).call(); commit(renamedName); Set<Path> files = newScmProvider().branchChangedFiles("master", worktree); assertThat(files).isNull(); }
@Test public void branchChangedFiles_from_diverged() throws IOException, GitAPIException { createAndCommitFile("file-m1.xoo"); createAndCommitFile("file-m2.xoo"); createAndCommitFile("file-m3.xoo"); ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); appendToAndCommitFile("file-m3.xoo"); createAndCommitFile("file-m4.xoo"); git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); git.checkout().setName("b1").call(); createAndCommitFile("file-b1.xoo"); appendToAndCommitFile("file-m1.xoo"); deleteAndCommitFile("file-m2.xoo"); assertThat(newScmProvider().branchChangedFiles("master", worktree)) .containsExactlyInAnyOrder( worktree.resolve("file-b1.xoo"), worktree.resolve("file-m1.xoo")); }
@Test public void branchChangedFiles_should_not_fail_with_patience_diff_algo() throws IOException { Path gitConfig = worktree.resolve(".git").resolve("config"); Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8)); Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); git = new Git(repo); assertThat(newScmProvider().branchChangedFiles("master", worktree)).isNull(); }