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

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