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.

SvnScmProvider.java 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 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.svn;
  21. import java.io.File;
  22. import java.net.MalformedURLException;
  23. import java.net.URISyntaxException;
  24. import java.net.URL;
  25. import java.nio.file.Path;
  26. import java.nio.file.Paths;
  27. import java.time.Instant;
  28. import java.util.HashSet;
  29. import java.util.Map;
  30. import java.util.Set;
  31. import javax.annotation.CheckForNull;
  32. import org.sonar.api.batch.scm.BlameCommand;
  33. import org.sonar.api.batch.scm.ScmProvider;
  34. import org.sonar.api.utils.log.Logger;
  35. import org.sonar.api.utils.log.Loggers;
  36. import org.tmatesoft.svn.core.SVNDepth;
  37. import org.tmatesoft.svn.core.SVNException;
  38. import org.tmatesoft.svn.core.SVNLogEntryPath;
  39. import org.tmatesoft.svn.core.SVNNodeKind;
  40. import org.tmatesoft.svn.core.SVNURL;
  41. import org.tmatesoft.svn.core.wc.SVNClientManager;
  42. import org.tmatesoft.svn.core.wc.SVNDiffClient;
  43. import org.tmatesoft.svn.core.wc.SVNInfo;
  44. import org.tmatesoft.svn.core.wc.SVNLogClient;
  45. import org.tmatesoft.svn.core.wc.SVNRevision;
  46. import org.tmatesoft.svn.core.wc.SVNWCClient;
  47. import static org.sonar.scm.svn.SvnScmSupport.newSvnClientManager;
  48. public class SvnScmProvider extends ScmProvider {
  49. private static final Logger LOG = Loggers.get(SvnScmProvider.class);
  50. private final SvnConfiguration configuration;
  51. private final SvnBlameCommand blameCommand;
  52. private final FindFork findFork;
  53. public SvnScmProvider(SvnConfiguration configuration, SvnBlameCommand blameCommand, FindFork findFork) {
  54. this.configuration = configuration;
  55. this.blameCommand = blameCommand;
  56. this.findFork = findFork;
  57. }
  58. @Override
  59. public String key() {
  60. return "svn";
  61. }
  62. @Override
  63. public boolean supports(File baseDir) {
  64. File folder = baseDir;
  65. while (folder != null) {
  66. if (new File(folder, ".svn").exists()) {
  67. return true;
  68. }
  69. folder = folder.getParentFile();
  70. }
  71. return false;
  72. }
  73. @Override
  74. public BlameCommand blameCommand() {
  75. return blameCommand;
  76. }
  77. @CheckForNull
  78. @Override
  79. public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) {
  80. SVNClientManager clientManager = null;
  81. try {
  82. clientManager = newSvnClientManager(configuration);
  83. return computeChangedPaths(rootBaseDir, clientManager);
  84. } catch (SVNException e) {
  85. LOG.warn(e.getMessage());
  86. } finally {
  87. if (clientManager != null) {
  88. try {
  89. clientManager.dispose();
  90. } catch (Exception e) {
  91. LOG.warn("Unable to dispose SVN ClientManager", e);
  92. }
  93. }
  94. }
  95. return null;
  96. }
  97. static Set<Path> computeChangedPaths(Path projectBasedir, SVNClientManager clientManager) throws SVNException {
  98. SVNWCClient wcClient = clientManager.getWCClient();
  99. SVNInfo svnInfo = wcClient.doInfo(projectBasedir.toFile(), null);
  100. // SVN path of the repo root, for example: /C:/Users/JANOSG~1/AppData/Local/Temp/x/y
  101. Path svnRootPath = toPath(svnInfo.getRepositoryRootURL());
  102. // the svn root path may be "" for urls like http://svnserver/
  103. // -> set it to "/" to avoid crashing when using Path.relativize later
  104. if (svnRootPath.equals(Paths.get(""))) {
  105. svnRootPath = Paths.get("/");
  106. }
  107. // SVN path of projectBasedir, for example: /C:/Users/JANOSG~1/AppData/Local/Temp/x/y/branches/b1
  108. Path svnProjectPath = toPath(svnInfo.getURL());
  109. // path of projectBasedir, as "absolute path within the SVN repo", for example: /branches/b1
  110. Path inRepoProjectPath = Paths.get("/").resolve(svnRootPath.relativize(svnProjectPath));
  111. // We inspect "svn log" from latest revision until copy-point.
  112. // The same path may appear in multiple commits, the ordering of changes and removals is important.
  113. Set<Path> paths = new HashSet<>();
  114. Set<Path> removed = new HashSet<>();
  115. SVNLogClient svnLogClient = clientManager.getLogClient();
  116. svnLogClient.doLog(new File[] {projectBasedir.toFile()}, null, null, null, true, true, 0, svnLogEntry ->
  117. svnLogEntry.getChangedPaths().values().forEach(entry -> {
  118. if (entry.getKind().equals(SVNNodeKind.FILE)) {
  119. Path path = projectBasedir.resolve(inRepoProjectPath.relativize(Paths.get(entry.getPath())));
  120. if (isModified(entry)) {
  121. // Skip if the path is removed in a more recent commit
  122. if (!removed.contains(path)) {
  123. paths.add(path);
  124. }
  125. } else if (entry.getType() == SVNLogEntryPath.TYPE_DELETED) {
  126. removed.add(path);
  127. }
  128. }
  129. }));
  130. return paths;
  131. }
  132. private static Path toPath(SVNURL svnUrl) {
  133. if ("file".equals(svnUrl.getProtocol())) {
  134. try {
  135. return Paths.get(new URL("file", svnUrl.getHost(), svnUrl.getPath()).toURI());
  136. } catch (URISyntaxException | MalformedURLException e) {
  137. throw new IllegalStateException(e);
  138. }
  139. }
  140. return Paths.get(svnUrl.getURIEncodedPath());
  141. }
  142. private static boolean isModified(SVNLogEntryPath entry) {
  143. return entry.getType() == SVNLogEntryPath.TYPE_ADDED
  144. || entry.getType() == SVNLogEntryPath.TYPE_MODIFIED;
  145. }
  146. @CheckForNull
  147. @Override
  148. public Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path rootBaseDir, Set<Path> changedFiles) {
  149. SVNClientManager clientManager = null;
  150. try {
  151. clientManager = newSvnClientManager(configuration);
  152. // find reference revision number: the copy point
  153. SVNLogClient svnLogClient = clientManager.getLogClient();
  154. long[] revisionCounter = {0};
  155. svnLogClient.doLog(new File[] {rootBaseDir.toFile()}, null, null, null, true, true, 0,
  156. svnLogEntry -> revisionCounter[0] = svnLogEntry.getRevision());
  157. long startRev = revisionCounter[0];
  158. SVNDiffClient svnDiffClient = clientManager.getDiffClient();
  159. File path = rootBaseDir.toFile();
  160. ChangedLinesComputer computer = newChangedLinesComputer(rootBaseDir, changedFiles);
  161. svnDiffClient.doDiff(path, SVNRevision.create(startRev), path, SVNRevision.WORKING, SVNDepth.INFINITY, false, computer.receiver(), null);
  162. return computer.changedLines();
  163. } catch (Exception e) {
  164. LOG.warn("Failed to get changed lines from Subversion", e);
  165. } finally {
  166. if (clientManager != null) {
  167. try {
  168. clientManager.dispose();
  169. } catch (Exception e) {
  170. LOG.warn("Unable to dispose SVN ClientManager", e);
  171. }
  172. }
  173. }
  174. return null;
  175. }
  176. @CheckForNull
  177. @Override
  178. public Instant forkDate(String referenceBranch, Path rootBaseDir) {
  179. try {
  180. return findFork.findDate(rootBaseDir, referenceBranch);
  181. } catch (SVNException e) {
  182. LOG.warn("Unable to find fork date with '" + referenceBranch + "'", e);
  183. return null;
  184. }
  185. }
  186. ChangedLinesComputer newChangedLinesComputer(Path rootBaseDir, Set<Path> changedFiles) {
  187. return new ChangedLinesComputer(rootBaseDir, changedFiles);
  188. }
  189. }