You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GitScmProviderTest.java 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.scm.git;
  21. import java.io.File;
  22. import java.io.IOException;
  23. import java.nio.charset.StandardCharsets;
  24. import java.nio.file.Files;
  25. import java.nio.file.Path;
  26. import java.nio.file.Paths;
  27. import java.nio.file.StandardOpenOption;
  28. import java.time.Instant;
  29. import java.time.temporal.ChronoUnit;
  30. import java.util.Arrays;
  31. import java.util.Collections;
  32. import java.util.Date;
  33. import java.util.HashSet;
  34. import java.util.LinkedHashSet;
  35. import java.util.List;
  36. import java.util.Map;
  37. import java.util.Random;
  38. import java.util.Set;
  39. import java.util.TimeZone;
  40. import java.util.concurrent.atomic.AtomicInteger;
  41. import org.eclipse.jgit.api.DiffCommand;
  42. import org.eclipse.jgit.api.Git;
  43. import org.eclipse.jgit.api.errors.GitAPIException;
  44. import org.eclipse.jgit.lib.ObjectId;
  45. import org.eclipse.jgit.lib.PersonIdent;
  46. import org.eclipse.jgit.lib.RefDatabase;
  47. import org.eclipse.jgit.lib.Repository;
  48. import org.eclipse.jgit.revwalk.RevCommit;
  49. import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
  50. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  51. import org.junit.Before;
  52. import org.junit.Rule;
  53. import org.junit.Test;
  54. import org.junit.rules.ExpectedException;
  55. import org.junit.rules.TemporaryFolder;
  56. import org.sonar.api.notifications.AnalysisWarnings;
  57. import org.sonar.api.scan.filesystem.PathResolver;
  58. import org.sonar.api.utils.MessageException;
  59. import org.sonar.api.utils.System2;
  60. import static java.util.Collections.emptySet;
  61. import static org.assertj.core.api.Assertions.assertThat;
  62. import static org.assertj.core.data.MapEntry.entry;
  63. import static org.mockito.ArgumentMatchers.any;
  64. import static org.mockito.ArgumentMatchers.anyBoolean;
  65. import static org.mockito.Mockito.mock;
  66. import static org.mockito.Mockito.verify;
  67. import static org.mockito.Mockito.verifyNoInteractions;
  68. import static org.mockito.Mockito.when;
  69. import static org.sonar.scm.git.Utils.javaUnzip;
  70. public class GitScmProviderTest {
  71. // Sample content for unified diffs
  72. // http://www.gnu.org/software/diffutils/manual/html_node/Example-Unified.html#Example-Unified
  73. private static final String CONTENT_LAO = "The Way that can be told of is not the eternal Way;\n"
  74. + "The name that can be named is not the eternal name.\n"
  75. + "The Nameless is the origin of Heaven and Earth;\n"
  76. + "The Named is the mother of all things.\n"
  77. + "Therefore let there always be non-being,\n"
  78. + " so we may see their subtlety,\n"
  79. + "And let there always be being,\n"
  80. + " so we may see their outcome.\n"
  81. + "The two are the same,\n"
  82. + "But after they are produced,\n"
  83. + " they have different names.\n";
  84. private static final String CONTENT_TZU = "The Nameless is the origin of Heaven and Earth;\n"
  85. + "The named is the mother of all things.\n"
  86. + "\n"
  87. + "Therefore let there always be non-being,\n"
  88. + " so we may see their subtlety,\n"
  89. + "And let there always be being,\n"
  90. + " so we may see their outcome.\n"
  91. + "The two are the same,\n"
  92. + "But after they are produced,\n"
  93. + " they have different names.\n"
  94. + "They both may be called deep and profound.\n"
  95. + "Deeper and more profound,\n"
  96. + "The door of all subtleties!";
  97. @Rule
  98. public TemporaryFolder temp = new TemporaryFolder();
  99. @Rule
  100. public ExpectedException thrown = ExpectedException.none();
  101. private GitIgnoreCommand gitIgnoreCommand = mock(GitIgnoreCommand.class);
  102. private static final Random random = new Random();
  103. private static final System2 system2 = mock(System2.class);
  104. private Path worktree;
  105. private Git git;
  106. private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
  107. @Before
  108. public void before() throws IOException, GitAPIException {
  109. worktree = temp.newFolder().toPath();
  110. Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
  111. repo.create();
  112. git = new Git(repo);
  113. createAndCommitFile("file-in-first-commit.xoo");
  114. }
  115. @Test
  116. public void sanityCheck() {
  117. assertThat(newGitScmProvider().key()).isEqualTo("git");
  118. }
  119. @Test
  120. public void returnImplem() {
  121. JGitBlameCommand jblameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings);
  122. GitScmProvider gitScmProvider = new GitScmProvider(jblameCommand, analysisWarnings, gitIgnoreCommand, system2);
  123. assertThat(gitScmProvider.blameCommand()).isEqualTo(jblameCommand);
  124. }
  125. /**
  126. * SONARSCGIT-47
  127. */
  128. @Test
  129. public void branchChangedFiles_should_not_crash_if_branches_have_no_common_ancestors() throws GitAPIException, IOException {
  130. String fileName = "file-in-first-commit.xoo";
  131. String renamedName = "file-renamed.xoo";
  132. git.checkout().setOrphan(true).setName("b1").call();
  133. Path file = worktree.resolve(fileName);
  134. Path renamed = file.resolveSibling(renamedName);
  135. addLineToFile(fileName, 1);
  136. Files.move(file, renamed);
  137. git.rm().addFilepattern(fileName).call();
  138. commit(renamedName);
  139. Set<Path> files = newScmProvider().branchChangedFiles("master", worktree);
  140. // no shared history, so no diff
  141. assertThat(files).isNull();
  142. }
  143. @Test
  144. public void testAutodetection() throws IOException {
  145. File baseDirEmpty = temp.newFolder();
  146. assertThat(newGitScmProvider().supports(baseDirEmpty)).isFalse();
  147. File projectDir = temp.newFolder();
  148. javaUnzip("dummy-git.zip", projectDir);
  149. File baseDir = new File(projectDir, "dummy-git");
  150. assertThat(newScmProvider().supports(baseDir)).isTrue();
  151. }
  152. private static JGitBlameCommand mockCommand() {
  153. return mock(JGitBlameCommand.class);
  154. }
  155. @Test
  156. public void branchChangedFiles_from_diverged() throws IOException, GitAPIException {
  157. createAndCommitFile("file-m1.xoo");
  158. createAndCommitFile("file-m2.xoo");
  159. createAndCommitFile("file-m3.xoo");
  160. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  161. appendToAndCommitFile("file-m3.xoo");
  162. createAndCommitFile("file-m4.xoo");
  163. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  164. git.checkout().setName("b1").call();
  165. createAndCommitFile("file-b1.xoo");
  166. appendToAndCommitFile("file-m1.xoo");
  167. deleteAndCommitFile("file-m2.xoo");
  168. assertThat(newScmProvider().branchChangedFiles("master", worktree))
  169. .containsExactlyInAnyOrder(
  170. worktree.resolve("file-b1.xoo"),
  171. worktree.resolve("file-m1.xoo"));
  172. }
  173. @Test
  174. public void branchChangedFiles_should_not_fail_with_patience_diff_algo() throws IOException {
  175. Path gitConfig = worktree.resolve(".git").resolve("config");
  176. Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8));
  177. Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
  178. git = new Git(repo);
  179. assertThat(newScmProvider().branchChangedFiles("master", worktree)).isNull();
  180. }
  181. @Test
  182. public void branchChangedFiles_from_merged_and_diverged() throws IOException, GitAPIException {
  183. createAndCommitFile("file-m1.xoo");
  184. createAndCommitFile("file-m2.xoo");
  185. createAndCommitFile("lao.txt", CONTENT_LAO);
  186. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  187. createAndCommitFile("file-m3.xoo");
  188. ObjectId mergePoint = git.getRepository().exactRef("HEAD").getObjectId();
  189. appendToAndCommitFile("file-m3.xoo");
  190. createAndCommitFile("file-m4.xoo");
  191. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  192. git.checkout().setName("b1").call();
  193. createAndCommitFile("file-b1.xoo");
  194. appendToAndCommitFile("file-m1.xoo");
  195. deleteAndCommitFile("file-m2.xoo");
  196. git.merge().include(mergePoint).call();
  197. createAndCommitFile("file-b2.xoo");
  198. createAndCommitFile("file-m5.xoo");
  199. deleteAndCommitFile("file-m5.xoo");
  200. Set<Path> changedFiles = newScmProvider().branchChangedFiles("master", worktree);
  201. assertThat(changedFiles)
  202. .containsExactlyInAnyOrder(
  203. worktree.resolve("file-m1.xoo"),
  204. worktree.resolve("file-b1.xoo"),
  205. worktree.resolve("file-b2.xoo"));
  206. // use a subset of changed files for .branchChangedLines to verify only requested files are returned
  207. assertThat(changedFiles.remove(worktree.resolve("file-b1.xoo"))).isTrue();
  208. // generate common sample diff
  209. createAndCommitFile("lao.txt", CONTENT_TZU);
  210. changedFiles.add(worktree.resolve("lao.txt"));
  211. // a file that should not yield any results
  212. changedFiles.add(worktree.resolve("nonexistent"));
  213. assertThat(newScmProvider().branchChangedLines("master", worktree, changedFiles))
  214. .containsOnly(
  215. entry(worktree.resolve("lao.txt"), new HashSet<>(Arrays.asList(2, 3, 11, 12, 13))),
  216. entry(worktree.resolve("file-m1.xoo"), new HashSet<>(Arrays.asList(4))),
  217. entry(worktree.resolve("file-b2.xoo"), new HashSet<>(Arrays.asList(1, 2, 3))));
  218. assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(worktree.resolve("nonexistent"))))
  219. .isEmpty();
  220. }
  221. @Test
  222. public void forkDate_from_diverged() throws IOException, GitAPIException {
  223. createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS));
  224. createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS));
  225. Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS);
  226. createAndCommitFile("file-m3.xoo", expectedForkDate);
  227. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  228. appendToAndCommitFile("file-m3.xoo");
  229. createAndCommitFile("file-m4.xoo");
  230. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  231. git.checkout().setName("b1").call();
  232. createAndCommitFile("file-b1.xoo");
  233. appendToAndCommitFile("file-m1.xoo");
  234. deleteAndCommitFile("file-m2.xoo");
  235. assertThat(newScmProvider().forkDate("master", worktree))
  236. .isEqualTo(expectedForkDate.truncatedTo(ChronoUnit.SECONDS));
  237. }
  238. @Test
  239. public void forkDate_should_not_fail_if_reference_is_the_same_branch() throws IOException, GitAPIException {
  240. createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS));
  241. createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS));
  242. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  243. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  244. git.checkout().setName("b1").call();
  245. Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS);
  246. createAndCommitFile("file-m3.xoo", expectedForkDate);
  247. assertThat(newScmProvider().forkDate("b1", worktree))
  248. .isEqualTo(expectedForkDate.truncatedTo(ChronoUnit.SECONDS));
  249. }
  250. @Test
  251. public void forkDate_should_not_fail_with_patience_diff_algo() throws IOException {
  252. Path gitConfig = worktree.resolve(".git").resolve("config");
  253. Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8));
  254. Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
  255. git = new Git(repo);
  256. assertThat(newScmProvider().forkDate("master", worktree)).isNull();
  257. }
  258. @Test
  259. public void forkDate_should_not_fail_with_invalid_basedir() throws IOException {
  260. assertThat(newScmProvider().forkDate("master", temp.newFolder().toPath())).isNull();
  261. }
  262. @Test
  263. public void forkDate_should_not_fail_when_no_merge_base_is_found() throws IOException, GitAPIException {
  264. createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS));
  265. git.checkout().setOrphan(true).setName("b1").call();
  266. createAndCommitFile("file-b1.xoo");
  267. assertThat(newScmProvider().forkDate("master", worktree)).isNull();
  268. }
  269. @Test
  270. public void forkDate_without_target_branch() throws IOException, GitAPIException {
  271. createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS));
  272. createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS));
  273. Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS);
  274. createAndCommitFile("file-m3.xoo", expectedForkDate);
  275. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  276. appendToAndCommitFile("file-m3.xoo");
  277. createAndCommitFile("file-m4.xoo");
  278. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  279. git.checkout().setName("b1").call();
  280. createAndCommitFile("file-b1.xoo");
  281. appendToAndCommitFile("file-m1.xoo");
  282. deleteAndCommitFile("file-m2.xoo");
  283. assertThat(newScmProvider().forkDate("unknown", worktree)).isNull();
  284. }
  285. @Test
  286. public void branchChangedLines_should_be_correct_when_change_is_not_committed() throws GitAPIException, IOException {
  287. String fileName = "file-in-first-commit.xoo";
  288. git.branchCreate().setName("b1").call();
  289. git.checkout().setName("b1").call();
  290. // this line is committed
  291. addLineToFile(fileName, 3);
  292. commit(fileName);
  293. // this line is not committed
  294. addLineToFile(fileName, 1);
  295. Path filePath = worktree.resolve(fileName);
  296. Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath));
  297. // both lines appear correctly
  298. assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4))));
  299. }
  300. @Test
  301. public void branchChangedLines_should_not_fail_if_there_is_no_merge_base() throws GitAPIException, IOException {
  302. createAndCommitFile("file-m1.xoo");
  303. git.checkout().setOrphan(true).setName("b1").call();
  304. createAndCommitFile("file-b1.xoo");
  305. Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(Paths.get("")));
  306. assertThat(changedLines).isNull();
  307. }
  308. @Test
  309. public void branchChangedLines_returns_empty_set_for_files_with_lines_removed_only() throws GitAPIException, IOException {
  310. String fileName = "file-in-first-commit.xoo";
  311. git.branchCreate().setName("b1").call();
  312. git.checkout().setName("b1").call();
  313. removeLineInFile(fileName, 2);
  314. commit(fileName);
  315. Path filePath = worktree.resolve(fileName);
  316. Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath));
  317. // both lines appear correctly
  318. assertThat(changedLines).containsExactly(entry(filePath, emptySet()));
  319. }
  320. @Test
  321. public void branchChangedLines_uses_relative_paths_from_project_root() throws GitAPIException, IOException {
  322. String fileName = "project1/file-in-first-commit.xoo";
  323. createAndCommitFile(fileName);
  324. git.branchCreate().setName("b1").call();
  325. git.checkout().setName("b1").call();
  326. // this line is committed
  327. addLineToFile(fileName, 3);
  328. commit(fileName);
  329. // this line is not committed
  330. addLineToFile(fileName, 1);
  331. Path filePath = worktree.resolve(fileName);
  332. Map<Path, Set<Integer>> changedLines = newScmProvider().branchChangedLines("master",
  333. worktree.resolve("project1"), Collections.singleton(filePath));
  334. // both lines appear correctly
  335. assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4))));
  336. }
  337. @Test
  338. public void branchChangedFiles_when_git_work_tree_is_above_project_basedir() throws IOException, GitAPIException {
  339. git.branchCreate().setName("b1").call();
  340. git.checkout().setName("b1").call();
  341. Path projectDir = worktree.resolve("project");
  342. Files.createDirectory(projectDir);
  343. createAndCommitFile("project/file-b1");
  344. assertThat(newScmProvider().branchChangedFiles("master", projectDir))
  345. .containsOnly(projectDir.resolve("file-b1"));
  346. }
  347. @Test
  348. public void branchChangedLines_should_not_fail_with_patience_diff_algo() throws IOException {
  349. Path gitConfig = worktree.resolve(".git").resolve("config");
  350. Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8));
  351. Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
  352. git = new Git(repo);
  353. assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(Paths.get("file")))).isNull();
  354. }
  355. /**
  356. * Unfortunately it looks like JGit doesn't support this setting using .gitattributes.
  357. */
  358. @Test
  359. public void branchChangedLines_should_always_ignore_different_line_endings() throws IOException, GitAPIException {
  360. Path filePath = worktree.resolve("file-m1.xoo");
  361. createAndCommitFile("file-m1.xoo");
  362. ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId();
  363. git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call();
  364. git.checkout().setName("b1").call();
  365. String newFileContent = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8).replaceAll("\n", "\r\n");
  366. Files.write(filePath, newFileContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING);
  367. commit("file-m1.xoo");
  368. assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath)))
  369. .isEmpty();
  370. }
  371. @Test
  372. public void branchChangedFiles_falls_back_to_origin_when_local_branch_does_not_exist() throws IOException, GitAPIException {
  373. git.branchCreate().setName("b1").call();
  374. git.checkout().setName("b1").call();
  375. createAndCommitFile("file-b1");
  376. Path worktree2 = temp.newFolder().toPath();
  377. Git.cloneRepository()
  378. .setURI(worktree.toString())
  379. .setDirectory(worktree2.toFile())
  380. .call();
  381. assertThat(newScmProvider().branchChangedFiles("master", worktree2))
  382. .containsOnly(worktree2.resolve("file-b1"));
  383. verifyNoInteractions(analysisWarnings);
  384. }
  385. @Test
  386. public void branchChangedFiles_use_remote_target_ref_when_running_on_circle_ci() throws IOException, GitAPIException {
  387. when(system2.envVariable("CIRCLECI")).thenReturn("true");
  388. git.checkout().setName("b1").setCreateBranch(true).call();
  389. createAndCommitFile("file-b1");
  390. Path worktree2 = temp.newFolder().toPath();
  391. Git local = Git.cloneRepository()
  392. .setURI(worktree.toString())
  393. .setDirectory(worktree2.toFile())
  394. .call();
  395. // Make local master match analyzed branch, so if local ref is used then change files will be empty
  396. local.checkout().setCreateBranch(true).setName("master").setStartPoint("origin/b1").call();
  397. local.checkout().setName("b1").call();
  398. assertThat(newScmProvider().branchChangedFiles("master", worktree2))
  399. .containsOnly(worktree2.resolve("file-b1"));
  400. verifyNoInteractions(analysisWarnings);
  401. }
  402. @Test
  403. public void branchChangedFiles_falls_back_to_local_ref_if_origin_branch_does_not_exist_when_running_on_circle_ci() throws IOException, GitAPIException {
  404. when(system2.envVariable("CIRCLECI")).thenReturn("true");
  405. git.checkout().setName("b1").setCreateBranch(true).call();
  406. createAndCommitFile("file-b1");
  407. Path worktree2 = temp.newFolder().toPath();
  408. Git local = Git.cloneRepository()
  409. .setURI(worktree.toString())
  410. .setDirectory(worktree2.toFile())
  411. .call();
  412. local.checkout().setName("local-only").setCreateBranch(true).setStartPoint("origin/master").call();
  413. local.checkout().setName("b1").call();
  414. assertThat(newScmProvider().branchChangedFiles("local-only", worktree2))
  415. .containsOnly(worktree2.resolve("file-b1"));
  416. verifyNoInteractions(analysisWarnings);
  417. }
  418. @Test
  419. public void branchChangedFiles_falls_back_to_upstream_ref() throws IOException, GitAPIException {
  420. git.branchCreate().setName("b1").call();
  421. git.checkout().setName("b1").call();
  422. createAndCommitFile("file-b1");
  423. Path worktree2 = temp.newFolder().toPath();
  424. Git.cloneRepository()
  425. .setURI(worktree.toString())
  426. .setRemote("upstream")
  427. .setDirectory(worktree2.toFile())
  428. .call();
  429. assertThat(newScmProvider().branchChangedFiles("master", worktree2))
  430. .containsOnly(worktree2.resolve("file-b1"));
  431. verifyNoInteractions(analysisWarnings);
  432. }
  433. @Test
  434. public void branchChangedFiles_finds_branch_in_specific_origin() throws IOException, GitAPIException {
  435. git.branchCreate().setName("b1").call();
  436. git.checkout().setName("b1").call();
  437. createAndCommitFile("file-b1");
  438. Path worktree2 = temp.newFolder().toPath();
  439. Git.cloneRepository()
  440. .setURI(worktree.toString())
  441. .setRemote("upstream")
  442. .setDirectory(worktree2.toFile())
  443. .call();
  444. assertThat(newScmProvider().branchChangedFiles("upstream/master", worktree2))
  445. .containsOnly(worktree2.resolve("file-b1"));
  446. verifyNoInteractions(analysisWarnings);
  447. }
  448. @Test
  449. public void branchChangedFiles_should_return_null_when_branch_nonexistent() {
  450. assertThat(newScmProvider().branchChangedFiles("nonexistent", worktree)).isNull();
  451. }
  452. @Test
  453. public void branchChangedFiles_should_throw_when_repo_nonexistent() throws IOException {
  454. thrown.expect(MessageException.class);
  455. thrown.expectMessage("Not inside a Git work tree: ");
  456. newScmProvider().branchChangedFiles("master", temp.newFolder().toPath());
  457. }
  458. @Test
  459. public void branchChangedFiles_should_throw_when_dir_nonexistent() {
  460. thrown.expect(MessageException.class);
  461. thrown.expectMessage("Not inside a Git work tree: ");
  462. newScmProvider().branchChangedFiles("master", temp.getRoot().toPath().resolve("nonexistent"));
  463. }
  464. @Test
  465. public void branchChangedFiles_should_return_null_on_io_errors_of_repo_builder() {
  466. GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
  467. @Override
  468. Repository buildRepo(Path basedir) throws IOException {
  469. throw new IOException();
  470. }
  471. };
  472. assertThat(provider.branchChangedFiles("branch", worktree)).isNull();
  473. verifyNoInteractions(analysisWarnings);
  474. }
  475. @Test
  476. public void branchChangedFiles_should_return_null_if_repo_exactref_is_null() throws IOException {
  477. Repository repository = mock(Repository.class);
  478. RefDatabase refDatabase = mock(RefDatabase.class);
  479. when(repository.getRefDatabase()).thenReturn(refDatabase);
  480. when(refDatabase.findRef("branch")).thenReturn(null);
  481. GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
  482. @Override
  483. Repository buildRepo(Path basedir) {
  484. return repository;
  485. }
  486. };
  487. assertThat(provider.branchChangedFiles("branch", worktree)).isNull();
  488. String warning = "Could not find ref 'branch' in refs/heads, refs/remotes, refs/remotes/upstream or refs/remotes/origin."
  489. + " You may see unexpected issues and changes. Please make sure to fetch this ref before pull request analysis.";
  490. verify(analysisWarnings).addUnique(warning);
  491. }
  492. @Test
  493. public void branchChangedFiles_should_return_null_on_errors() throws GitAPIException {
  494. DiffCommand diffCommand = mock(DiffCommand.class);
  495. when(diffCommand.setShowNameAndStatusOnly(anyBoolean())).thenReturn(diffCommand);
  496. when(diffCommand.setOldTree(any())).thenReturn(diffCommand);
  497. when(diffCommand.setNewTree(any())).thenReturn(diffCommand);
  498. when(diffCommand.call()).thenThrow(mock(GitAPIException.class));
  499. Git git = mock(Git.class);
  500. when(git.diff()).thenReturn(diffCommand);
  501. GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
  502. @Override
  503. Git newGit(Repository repo) {
  504. return git;
  505. }
  506. };
  507. assertThat(provider.branchChangedFiles("master", worktree)).isNull();
  508. verify(diffCommand).call();
  509. }
  510. @Test
  511. public void branchChangedLines_returns_null_when_branch_doesnt_exist() {
  512. assertThat(newScmProvider().branchChangedLines("nonexistent", worktree, emptySet())).isNull();
  513. }
  514. @Test
  515. public void branchChangedLines_omits_files_with_git_api_errors() throws IOException, GitAPIException {
  516. String f1 = "file-in-first-commit.xoo";
  517. String f2 = "file2-in-first-commit.xoo";
  518. createAndCommitFile(f2);
  519. git.branchCreate().setName("b1").call();
  520. git.checkout().setName("b1").call();
  521. // both files modified
  522. addLineToFile(f1, 1);
  523. addLineToFile(f2, 2);
  524. commit(f1);
  525. commit(f2);
  526. AtomicInteger callCount = new AtomicInteger(0);
  527. GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
  528. @Override
  529. AbstractTreeIterator prepareTreeParser(Repository repo, RevCommit commit) throws IOException {
  530. if (callCount.getAndIncrement() == 1) {
  531. throw new RuntimeException("error");
  532. }
  533. return super.prepareTreeParser(repo, commit);
  534. }
  535. };
  536. Set<Path> changedFiles = new LinkedHashSet<>();
  537. changedFiles.add(worktree.resolve(f1));
  538. changedFiles.add(worktree.resolve(f2));
  539. assertThat(provider.branchChangedLines("master", worktree, changedFiles))
  540. .isEqualTo(Collections.singletonMap(worktree.resolve(f1), Collections.singleton(1)));
  541. }
  542. @Test
  543. public void branchChangedLines_returns_null_on_io_errors_of_repo_builder() {
  544. GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
  545. @Override
  546. Repository buildRepo(Path basedir) throws IOException {
  547. throw new IOException();
  548. }
  549. };
  550. assertThat(provider.branchChangedLines("branch", worktree, emptySet())).isNull();
  551. }
  552. @Test
  553. public void relativePathFromScmRoot_should_return_dot_project_root() {
  554. assertThat(newGitScmProvider().relativePathFromScmRoot(worktree)).isEqualTo(Paths.get(""));
  555. }
  556. private GitScmProvider newGitScmProvider() {
  557. return new GitScmProvider(mock(JGitBlameCommand.class), analysisWarnings, gitIgnoreCommand, system2);
  558. }
  559. @Test
  560. public void relativePathFromScmRoot_should_return_filename_for_file_in_project_root() throws IOException {
  561. Path filename = Paths.get("somefile.xoo");
  562. Path path = worktree.resolve(filename);
  563. Files.createFile(path);
  564. assertThat(newGitScmProvider().relativePathFromScmRoot(path)).isEqualTo(filename);
  565. }
  566. @Test
  567. public void relativePathFromScmRoot_should_return_relative_path_for_file_in_project_subdir() throws IOException {
  568. Path relpath = Paths.get("sub/dir/to/somefile.xoo");
  569. Path path = worktree.resolve(relpath);
  570. Files.createDirectories(path.getParent());
  571. Files.createFile(path);
  572. assertThat(newGitScmProvider().relativePathFromScmRoot(path)).isEqualTo(relpath);
  573. }
  574. @Test
  575. public void revisionId_should_return_different_sha1_after_commit() throws IOException, GitAPIException {
  576. Path projectDir = worktree.resolve("project");
  577. Files.createDirectory(projectDir);
  578. GitScmProvider provider = newGitScmProvider();
  579. String sha1before = provider.revisionId(projectDir);
  580. assertThat(sha1before).hasSize(40);
  581. createAndCommitFile("project/file1");
  582. String sha1after = provider.revisionId(projectDir);
  583. assertThat(sha1after).hasSize(40);
  584. assertThat(sha1after).isNotEqualTo(sha1before);
  585. assertThat(provider.revisionId(projectDir)).isEqualTo(sha1after);
  586. }
  587. @Test
  588. public void revisionId_should_return_null_in_empty_repo() throws IOException {
  589. worktree = temp.newFolder().toPath();
  590. Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
  591. repo.create();
  592. git = new Git(repo);
  593. Path projectDir = worktree.resolve("project");
  594. Files.createDirectory(projectDir);
  595. GitScmProvider provider = newGitScmProvider();
  596. assertThat(provider.revisionId(projectDir)).isNull();
  597. }
  598. private String randomizedContent(String prefix, int numLines) {
  599. StringBuilder sb = new StringBuilder();
  600. for (int line = 0; line < numLines; line++) {
  601. sb.append(randomizedLine(prefix));
  602. sb.append("\n");
  603. }
  604. return sb.toString();
  605. }
  606. private String randomizedLine(String prefix) {
  607. StringBuilder sb = new StringBuilder(prefix);
  608. for (int i = 0; i < 4; i++) {
  609. sb.append(' ');
  610. for (int j = 0; j < prefix.length(); j++) {
  611. sb.append((char) ('a' + random.nextInt(26)));
  612. }
  613. }
  614. return sb.toString();
  615. }
  616. private void createAndCommitFile(String relativePath) throws IOException, GitAPIException {
  617. createAndCommitFile(relativePath, randomizedContent(relativePath, 3));
  618. }
  619. private void createAndCommitFile(String relativePath, Instant commitDate) throws IOException, GitAPIException {
  620. createFile(relativePath, randomizedContent(relativePath, 3));
  621. commit(relativePath, commitDate);
  622. }
  623. private void createAndCommitFile(String relativePath, String content) throws IOException, GitAPIException {
  624. createFile(relativePath, content);
  625. commit(relativePath);
  626. }
  627. private void createFile(String relativePath, String content) throws IOException {
  628. Path newFile = worktree.resolve(relativePath);
  629. Files.createDirectories(newFile.getParent());
  630. Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE);
  631. }
  632. private void addLineToFile(String relativePath, int lineNumber) throws IOException {
  633. Path filePath = worktree.resolve(relativePath);
  634. List<String> lines = Files.readAllLines(filePath);
  635. lines.add(lineNumber - 1, randomizedLine(relativePath));
  636. Files.write(filePath, lines, StandardOpenOption.TRUNCATE_EXISTING);
  637. }
  638. private void removeLineInFile(String relativePath, int lineNumber) throws IOException {
  639. Path filePath = worktree.resolve(relativePath);
  640. List<String> lines = Files.readAllLines(filePath);
  641. lines.remove(lineNumber - 1);
  642. Files.write(filePath, lines, StandardOpenOption.TRUNCATE_EXISTING);
  643. }
  644. private void appendToAndCommitFile(String relativePath) throws IOException, GitAPIException {
  645. Files.write(worktree.resolve(relativePath), randomizedContent(relativePath, 1).getBytes(), StandardOpenOption.APPEND);
  646. commit(relativePath);
  647. }
  648. private void deleteAndCommitFile(String relativePath) throws GitAPIException {
  649. git.rm().addFilepattern(relativePath).call();
  650. commit(relativePath);
  651. }
  652. private void commit(String... relativePaths) throws GitAPIException {
  653. for (String path : relativePaths) {
  654. git.add().addFilepattern(path).call();
  655. }
  656. String msg = String.join(",", relativePaths);
  657. git.commit().setAuthor("joe", "joe@example.com").setMessage(msg).call();
  658. }
  659. private void commit(String relativePath, Instant date) throws GitAPIException {
  660. PersonIdent person = new PersonIdent("joe", "joe@example.com", Date.from(date), TimeZone.getDefault());
  661. git.commit().setAuthor(person).setCommitter(person).setMessage(relativePath).call();
  662. }
  663. private GitScmProvider newScmProvider() {
  664. return new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2);
  665. }
  666. }