GitScmProvider extends ScmProvider { @CheckForNull @Override public Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path projectBaseDir, Set<Path> changedFiles) { try (Repository repo = buildRepo(projectBaseDir)) { 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())) { return null; } repo.getConfig().setBoolean("core", null, "autocrlf", true); Optional<RevCommit> mergeBaseCommit = findMergeBase(repo, targetRef); if (!mergeBaseCommit.isPresent()) { LOG.warn("No merge base found between HEAD and " + targetRef.getName()); return null; } Map<Path, Set<Integer>> changedLines = new HashMap<>(); Path repoRootDir = repo.getDirectory().toPath().getParent(); for (Path path : changedFiles) { collectChangedLines(repo, mergeBaseCommit.get(), changedLines, repoRootDir, path); } return changedLines; } catch (Exception e) { LOG.warn("Failed to get changed lines from git", 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 branchChangedLines_should_be_correct_when_change_is_not_committed() throws GitAPIException, IOException { String fileName = "file-in-first-commit.xoo"; git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); addLineToFile(fileName, 3); commit(fileName); addLineToFile(fileName, 1); Path filePath = worktree.resolve(fileName); Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath)); assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4)))); }
@Test public void branchChangedLines_should_not_fail_if_there_is_no_merge_base() throws GitAPIException, IOException { createAndCommitFile("file-m1.xoo"); git.checkout().setOrphan(true).setName("b1").call(); createAndCommitFile("file-b1.xoo"); Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(Paths.get(""))); assertThat(changedLines).isNull(); }
@Test public void branchChangedLines_returns_empty_set_for_files_with_lines_removed_only() throws GitAPIException, IOException { String fileName = "file-in-first-commit.xoo"; git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); removeLineInFile(fileName, 2); commit(fileName); Path filePath = worktree.resolve(fileName); Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath)); assertThat(changedLines).containsExactly(entry(filePath, emptySet())); }
@Test public void branchChangedLines_uses_relative_paths_from_project_root() throws GitAPIException, IOException { String fileName = "project1/file-in-first-commit.xoo"; createAndCommitFile(fileName); git.branchCreate().setName("b1").call(); git.checkout().setName("b1").call(); addLineToFile(fileName, 3); commit(fileName); addLineToFile(fileName, 1); Path filePath = worktree.resolve(fileName); Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree.resolve("project1"), Collections.singleton(filePath)); assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4)))); }
@Test public void branchChangedLines_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().branchChangedLines("master", worktree, Collections.singleton(Paths.get("file")))).isNull(); }
@Test public void branchChangedLines_should_always_ignore_different_line_endings() throws IOException, GitAPIException { Path filePath = worktree.resolve("file-m1.xoo"); createAndCommitFile("file-m1.xoo"); ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); git.checkout().setName("b1").call(); String newFileContent = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8).replaceAll("\n", "\r\n"); Files.write(filePath, newFileContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING); commit("file-m1.xoo"); assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath))) .isEmpty(); }
@Test public void branchChangedLines_returns_null_when_branch_doesnt_exist() { assertThat(newScmProvider().branchChangedLines("nonexistent", worktree, emptySet())).isNull(); }
@Test public void branchChangedLines_returns_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.branchChangedLines("branch", worktree, emptySet())).isNull(); }