├── .cirrus.yml ├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── its ├── pom.xml ├── scm-repo │ ├── 1.6 │ │ ├── repo-svn-with-merge.zip │ │ └── repo-svn.zip │ └── 1.8 │ │ ├── repo-svn-with-merge.zip │ │ └── repo-svn.zip └── src │ └── test │ └── java │ └── com │ └── sonarsource │ └── it │ └── scm │ └── SvnTest.java ├── pom.xml ├── sonar-scm-svn-plugin ├── pom.xml ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── sonar │ │ │ └── plugins │ │ │ └── scm │ │ │ └── svn │ │ │ ├── AnnotationHandler.java │ │ │ ├── ChangedLinesComputer.java │ │ │ ├── FindFork.java │ │ │ ├── ForkPoint.java │ │ │ ├── SvnBlameCommand.java │ │ │ ├── SvnConfiguration.java │ │ │ ├── SvnPlugin.java │ │ │ ├── SvnScmProvider.java │ │ │ └── package-info.java │ └── test │ │ ├── java │ │ └── org │ │ │ └── sonar │ │ │ └── plugins │ │ │ └── scm │ │ │ └── svn │ │ │ ├── ChangedLinesComputerTest.java │ │ │ ├── FindForkTest.java │ │ │ ├── SvnBlameCommandTest.java │ │ │ ├── SvnConfigurationTest.java │ │ │ ├── SvnPluginTest.java │ │ │ ├── SvnScmProviderTest.java │ │ │ ├── SvnTester.java │ │ │ └── SvnTesterTest.java │ │ └── resources │ │ ├── blame-with-anonymous-commit.xml │ │ ├── blame-with-merge-history.xml │ │ ├── blame-with-uncomitted-changes.xml │ │ └── blame.xml └── test-repos │ ├── 1.6 │ ├── repo-svn-with-merge.zip │ └── repo-svn.zip │ ├── 1.7 │ ├── repo-svn-with-merge.zip │ └── repo-svn.zip │ ├── 1.8 │ ├── repo-svn-with-merge.zip │ └── repo-svn.zip │ └── 1.9 │ ├── repo-svn-with-merge.zip │ └── repo-svn.zip └── third-party-licenses.sh /.cirrus.yml: -------------------------------------------------------------------------------- 1 | # content of service-account-credentials.json, used to access to Google Cloud Platform 2 | gcp_credentials: ENCRYPTED[!e5f7207bd8d02d383733bef47e18296ac32e3b7d22eb480354e8dd8fdc0004be45a8a4e72c797bd66ee94eb3340fa363!] 3 | 4 | # 5 | # ENV VARIABLES 6 | # 7 | env: 8 | ### Shared variables 9 | ARTIFACTORY_URL: ENCRYPTED[!2f8fa307d3289faa0aa6791f18b961627ae44f1ef46b136e1a1e63b0b4c86454dbb25520d49b339e2d50a1e1e5f95c88!] 10 | ARTIFACTORY_PRIVATE_USERNAME: repox-private-reader 11 | ARTIFACTORY_PRIVATE_PASSWORD: ENCRYPTED[!35ca4446564213d4fd2d1a96e42a871d5de6e6aac4e1dd3e89342892a60a2badf74a966bcc8e885e9c9d09a775ffe4c0!] 12 | ARTIFACTORY_API_KEY: ENCRYPTED[!35ca4446564213d4fd2d1a96e42a871d5de6e6aac4e1dd3e89342892a60a2badf74a966bcc8e885e9c9d09a775ffe4c0!] 13 | ARTIFACTORY_DEPLOY_USERNAME: repox-qa-deployer 14 | ARTIFACTORY_DEPLOY_PASSWORD: ENCRYPTED[!d484e19f33c9ce63b165f70e414a33b1ac6c215a126791aacbf8059626caf0fd8a78e999a20af5c1a4ba01c0b0247921!] 15 | ARTIFACTORY_DEPLOY_REPO: sonarsource-public-qa 16 | 17 | GCF_ACCESS_TOKEN: ENCRYPTED[!1fb91961a5c01e06e38834e55755231d649dc62eca354593105af9f9d643d701ae4539ab6a8021278b8d9348ae2ce8be!] 18 | PROMOTE_URL: ENCRYPTED[!e22ed2e34a8f7a1aea5cff653585429bbd3d5151e7201022140218f9c5d620069ec2388f14f83971e3fd726215bc0f5e!] 19 | 20 | GITHUB_TOKEN: ENCRYPTED[!f272985ea5b49b3cf9c414b98de6a8e9096be47bfcee52f33311ba3131a2af637c1b956f49585b7757dd84b7c030233a!] 21 | 22 | BURGR_URL: ENCRYPTED[!c7e294da94762d7bac144abef6310c5db300c95979daed4454ca977776bfd5edeb557e1237e3aa8ed722336243af2d78!] 23 | BURGR_USERNAME: ENCRYPTED[!b29ddc7610116de511e74bec9a93ad9b8a20ac217a0852e94a96d0066e6e822b95e7bc1fe152afb707f16b70605fddd3!] 24 | BURGR_PASSWORD: ENCRYPTED[!83e130718e92b8c9de7c5226355f730e55fb46e45869149a9223e724bb99656878ef9684c5f8cfef434aa716e87f4cf2!] 25 | 26 | ### Project variables 27 | DEPLOY_PULL_REQUEST: true 28 | ARTIFACTS: org.sonarsource.scm.svn:sonar-scm-svn:jar 29 | 30 | # 31 | # RE-USABLE CONFIGS 32 | # 33 | container_definition: &CONTAINER_DEFINITION 34 | image: us.gcr.io/sonarqube-team/base:j11-m3-latest 35 | cluster_name: cirrus-ci-cluster 36 | zone: us-central1-a 37 | namespace: default 38 | 39 | only_sonarsource_qa: &ONLY_SONARSOURCE_QA 40 | only_if: $CIRRUS_USER_COLLABORATOR == 'true' && ($CIRRUS_PR != "" || $CIRRUS_BRANCH == "master" || $CIRRUS_BRANCH =~ "branch-.*" || $CIRRUS_BRANCH =~ "dogfood-on-.*") 41 | 42 | # 43 | # TASKS 44 | # 45 | build_task: 46 | gke_container: 47 | <<: *CONTAINER_DEFINITION 48 | cpu: 2 49 | memory: 2G 50 | env: 51 | SONAR_TOKEN: ENCRYPTED[!b6fd814826c51e64ee61b0b6f3ae621551f6413383f7170f73580e2e141ac78c4b134b506f6288c74faa0dd564c05a29!] 52 | SONAR_HOST_URL: https://next.sonarqube.com/sonarqube 53 | maven_cache: 54 | folder: ${CIRRUS_WORKING_DIR}/.m2/repository 55 | script: 56 | - source cirrus-env BUILD 57 | - regular_mvn_build_deploy_analyze 58 | cleanup_before_cache_script: 59 | - cleanup_maven_repository 60 | 61 | qa_task: 62 | depends_on: 63 | - build 64 | <<: *ONLY_SONARSOURCE_QA 65 | gke_container: 66 | <<: *CONTAINER_DEFINITION 67 | cpu: 1.7 68 | memory: 5Gb 69 | env: 70 | matrix: 71 | - SQ_VERSION: LATEST_RELEASE[7.9] 72 | maven_cache: 73 | folder: ${CIRRUS_WORKING_DIR}/.m2/repository 74 | qa_script: 75 | - source cirrus-env QA 76 | - source set_maven_build_version $BUILD_NUMBER 77 | - cd its 78 | - mvn -Dsonar.runtimeVersion="$SQ_VERSION" -Dmaven.test.redirectTestOutputToFile=false verify -B -e -V 79 | cleanup_before_cache_script: 80 | - cleanup_maven_repository 81 | 82 | promote_task: 83 | depends_on: 84 | - qa 85 | <<: *ONLY_SONARSOURCE_QA 86 | gke_container: 87 | <<: *CONTAINER_DEFINITION 88 | cpu: 0.5 89 | memory: 500M 90 | maven_cache: 91 | folder: $CIRRUS_WORKING_DIR/.m2/repository 92 | script: 93 | - cirrus_promote_maven 94 | cleanup_before_cache_script: 95 | - cleanup_maven_repository 96 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | # This workflow is triggered when publishing a GitHub release 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | name: Start release process 12 | steps: 13 | # Not sure why this is needed... Fixes issue with running the action. 14 | - name: Checkout release action 15 | uses: actions/checkout@v2 16 | with: 17 | repository: SonarSource/gh-action_LT_release 18 | - name: Run release action 19 | id: run_release 20 | with: 21 | distribute: true 22 | uses: SonarSource/gh-action_LT_release@master 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 25 | - name: Log outputs 26 | if: always() 27 | run: | 28 | echo "${{ steps.run_release.outputs.releasability }}" 29 | echo "${{ steps.run_release.outputs.release }}" 30 | - name: Notify success on Slack 31 | uses: Ilshidur/action-slack@2.0.0 32 | env: 33 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 34 | with: 35 | args: "Release successful for {{ GITHUB_REPOSITORY }} by {{ GITHUB_ACTOR }}" 36 | - name: Notify failures on Slack 37 | uses: Ilshidur/action-slack@2.0.0 38 | if: failure() 39 | env: 40 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 41 | with: 42 | args: "Release failed, see the logs at https://github.com/{{ GITHUB_REPOSITORY }}/actions by {{ GITHUB_ACTOR }}" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The following should be moved in related sub-directories 2 | server/sonar-web/src/main/webapp/stylesheets/sonar-colorizer.css 3 | server/sonar-web/src/main/webapp/deploy/plugins 4 | server/sonar-web/src/main/webapp/deploy/bootstrap 5 | server/sonar-web/src/main/webapp/deploy/maven/org 6 | server/sonar-web/src/main/webapp/WEB-INF/log/ 7 | server/sonar-web/src/main/webapp/deploy/*.jar 8 | server/sonar-web/src/main/webapp/deploy/jdbc-driver.txt 9 | 10 | 11 | # ---- Javadoc 12 | docs.tar 13 | 14 | # ---- Maven 15 | target/ 16 | dependency-reduced-pom.xml 17 | 18 | # ---- IntelliJ IDEA 19 | *.iws 20 | *.iml 21 | *.ipr 22 | .idea/ 23 | 24 | # ---- Eclipse 25 | .classpath 26 | .project 27 | .settings 28 | .externalToolBuilders 29 | 30 | # ---- Mac OS X 31 | .DS_Store 32 | Icon? 33 | # Thumbnails 34 | ._* 35 | # Files that might appear on external disk 36 | .Spotlight-V100 37 | .Trashes 38 | 39 | # ---- Windows 40 | # Windows image file caches 41 | Thumbs.db 42 | # Folder config file 43 | Desktop.ini 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | SonarQube :: Plugins :: SCM :: SVN 2 | Copyright (C) 2014-2017 SonarSource SA 3 | mailto:info AT sonarsource DOT com 4 | 5 | This product includes software developed at 6 | SonarSource (http://www.sonarsource.com/). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SonarQube SVN Plugin 2 | 3 | [![Build Status](https://api.cirrus-ci.com/github/SonarSource/sonar-scm-svn.svg)](https://cirrus-ci.com/github/SonarSource/sonar-scm-svn) 4 | 5 | ### Enbedded since SonarQube 8.5 6 | 7 | This plugin is embedded in SonarQube starting 8.5, see [embedded sources](https://github.com/SonarSource/sonarqube/tree/master/sonar-scanner-engine/src/main/java/org/sonar/scm/svn). 8 | 9 | ### Have Question or Feedback? 10 | 11 | For support questions ("How do I?", "I got this error, why?", ...), please head to the [SonarSource forum](https://community.sonarsource.com/c/help). There are chances that a question similar to yours has already been answered. 12 | 13 | Be aware that this forum is a community, so the standard pleasantries ("Hi", "Thanks", ...) are expected. And if you don't get an answer to your thread, you should sit on your hands for at least three days before bumping it. Operators are not standing by. :-) 14 | 15 | ### Contributing 16 | 17 | If you would like to see a new feature, please create a new thread in the forum ["Suggest new features"](https://community.sonarsource.com/c/suggestions/features). 18 | 19 | Please be aware that we are not actively looking for feature contributions. The truth is that it's extremely difficult for someone outside SonarSource to comply with our roadmap and expectations. Therefore, we typically only accept minor cosmetic changes and typo fixes. 20 | 21 | With that in mind, if you would like to submit a code contribution, please create a pull request for this repository. Please explain your motives to contribute this change: what problem you are trying to fix, what improvement you are trying to make. 22 | 23 | Make sure that you follow our code style and all tests are passing (Travis build is executed for each pull request). 24 | 25 | ### License 26 | 27 | Copyright 2014-2017 SonarSource. 28 | 29 | Licensed under the [GNU Lesser General Public License, Version 3.0](http://www.gnu.org/licenses/lgpl.txt) 30 | -------------------------------------------------------------------------------- /its/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.sonarsource.scm.svn 8 | svn 9 | 1.10-SNAPSHOT 10 | 11 | 12 | org.sonarsource.scm.svn 13 | it-scmsvn 14 | SVN :: Integration Tests 15 | 16 | 2014 17 | 18 | 19 | 20 | org.sonarsource.orchestrator 21 | sonar-orchestrator 22 | 3.22.0.1791 23 | test 24 | 25 | 26 | junit 27 | junit 28 | 4.13.1 29 | test 30 | 31 | 32 | org.assertj 33 | assertj-core 34 | 3.11.1 35 | test 36 | 37 | 38 | org.tmatesoft.svnkit 39 | svnkit 40 | 1.9.3 41 | test 42 | 43 | 44 | 45 | 46 | 47 | qa 48 | 49 | 50 | env.SONARSOURCE_QA 51 | true 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-dependency-plugin 59 | 2.10 60 | 61 | 62 | copy-plugin 63 | generate-test-resources 64 | 65 | copy 66 | 67 | 68 | 69 | 70 | ${project.groupId} 71 | sonar-scm-svn-plugin 72 | ${project.version} 73 | sonar-plugin 74 | true 75 | 76 | 77 | ../sonar-scm-svn-plugin/target 78 | true 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /its/scm-repo/1.6/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/its/scm-repo/1.6/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /its/scm-repo/1.6/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/its/scm-repo/1.6/repo-svn.zip -------------------------------------------------------------------------------- /its/scm-repo/1.8/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/its/scm-repo/1.8/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /its/scm-repo/1.8/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/its/scm-repo/1.8/repo-svn.zip -------------------------------------------------------------------------------- /its/src/test/java/com/sonarsource/it/scm/SvnTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SVN :: Integration Tests 3 | * Copyright (C) 2014-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 com.sonarsource.it.scm; 21 | 22 | import com.sonar.orchestrator.Orchestrator; 23 | import com.sonar.orchestrator.build.BuildResult; 24 | import com.sonar.orchestrator.build.MavenBuild; 25 | import com.sonar.orchestrator.locator.FileLocation; 26 | import com.sonar.orchestrator.locator.MavenLocation; 27 | import com.sonar.orchestrator.util.ZipUtils; 28 | import java.io.File; 29 | import java.io.IOException; 30 | import java.nio.charset.StandardCharsets; 31 | import java.nio.file.Files; 32 | import java.text.ParseException; 33 | import java.text.SimpleDateFormat; 34 | import java.util.Arrays; 35 | import java.util.Date; 36 | import java.util.HashMap; 37 | import java.util.Map; 38 | import org.apache.commons.io.FileUtils; 39 | import org.apache.commons.lang.StringUtils; 40 | import org.apache.commons.lang.builder.EqualsBuilder; 41 | import org.apache.commons.lang.builder.HashCodeBuilder; 42 | import org.apache.commons.lang.builder.ToStringBuilder; 43 | import org.apache.commons.lang.builder.ToStringStyle; 44 | import org.assertj.core.data.MapEntry; 45 | import org.junit.Before; 46 | import org.junit.ClassRule; 47 | import org.junit.Rule; 48 | import org.junit.Test; 49 | import org.junit.rules.ExpectedException; 50 | import org.junit.rules.TemporaryFolder; 51 | import org.junit.runner.RunWith; 52 | import org.junit.runners.Parameterized; 53 | import org.junit.runners.Parameterized.Parameters; 54 | import org.sonar.wsclient.jsonsimple.JSONArray; 55 | import org.sonar.wsclient.jsonsimple.JSONObject; 56 | import org.sonar.wsclient.jsonsimple.JSONValue; 57 | import org.tmatesoft.svn.core.SVNDepth; 58 | import org.tmatesoft.svn.core.SVNURL; 59 | import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 60 | import org.tmatesoft.svn.core.internal.wc2.compat.SvnCodec; 61 | import org.tmatesoft.svn.core.wc.ISVNOptions; 62 | import org.tmatesoft.svn.core.wc.SVNClientManager; 63 | import org.tmatesoft.svn.core.wc.SVNRevision; 64 | import org.tmatesoft.svn.core.wc.SVNUpdateClient; 65 | import org.tmatesoft.svn.core.wc.SVNWCUtil; 66 | import org.tmatesoft.svn.core.wc2.SvnCheckout; 67 | import org.tmatesoft.svn.core.wc2.SvnTarget; 68 | 69 | import static org.apache.commons.lang.StringUtils.substringAfter; 70 | import static org.assertj.core.api.Assertions.assertThat; 71 | 72 | @RunWith(Parameterized.class) 73 | public class SvnTest { 74 | 75 | @Rule 76 | public TemporaryFolder temp = new TemporaryFolder(); 77 | 78 | public static final File REPO_DIR = new File("scm-repo"); 79 | 80 | @ClassRule 81 | public static Orchestrator orchestrator = Orchestrator.builderEnv() 82 | .setSonarVersion(System.getProperty("sonar.runtimeVersion", "LATEST_RELEASE[7.9]")) 83 | .addPlugin(FileLocation.byWildcardMavenFilename(new File("../sonar-scm-svn-plugin/target"), "sonar-scm-svn-plugin-*.jar")) 84 | .addPlugin(MavenLocation.of("org.sonarsource.java", "sonar-java-plugin", "LATEST_RELEASE")) 85 | .build(); 86 | 87 | @Rule 88 | public ExpectedException thrown = ExpectedException.none(); 89 | 90 | private String serverVersion; 91 | private int wcVersion; 92 | private String wkSubPath; 93 | private String baseDirSubPath; 94 | 95 | @Parameters(name = "SVN server version {0}, WC version {1}, WC subPath \"{2}\", baseDir subPath \"{3}\"") 96 | public static Iterable data() { 97 | return Arrays.asList(new Object[][] {{"1.6", 10, "dummy-svn", ""}, {"1.6", 10, "", "dummy-svn"}, {"1.8", 31, "dummy-svn", ""}, {"1.8", 31, "", "dummy-svn"}}); 98 | } 99 | 100 | public SvnTest(String serverVersion, int wcVersion, String wkSubPath, String baseDirSubPath) { 101 | this.serverVersion = serverVersion; 102 | this.wcVersion = wcVersion; 103 | 104 | // SONARSCSVN-11: Manage the case of a project baseDir is in a subFolder of working copy 105 | this.wkSubPath = wkSubPath; 106 | this.baseDirSubPath = baseDirSubPath; 107 | } 108 | 109 | @Before 110 | public void deleteData() { 111 | orchestrator.resetData(); 112 | } 113 | 114 | @Test 115 | public void sample_svn_project() throws Exception { 116 | File repo = unzip("repo-svn.zip"); 117 | 118 | String scmUrl = "file:///" + unixPath(new File(repo, "repo-svn/" + wkSubPath)); 119 | 120 | runSonar(new File(checkout(scmUrl), baseDirSubPath)); 121 | 122 | assertThat(getScmData("dummy:dummy:src/main/java/org/dummy/Dummy.java")) 123 | .contains( 124 | MapEntry.entry(1, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot")), 125 | MapEntry.entry(2, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot")), 126 | MapEntry.entry(3, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot")), 127 | MapEntry.entry(24, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot"))); 128 | } 129 | 130 | // SONARSCSVN-4, SONARSCSVN-5 131 | @Test 132 | public void dont_fail_on_uncommited_files() throws Exception { 133 | File repo = unzip("repo-svn.zip"); 134 | 135 | String scmUrl = "file:///" + unixPath(new File(repo, "repo-svn/" + wkSubPath)); 136 | File baseDir = new File(checkout(scmUrl), baseDirSubPath); 137 | 138 | // Edit file 139 | FileUtils.write(new File(baseDir, "src/main/java/org/dummy/Dummy.java"), "\n//bla\n//bla", StandardCharsets.UTF_8, true); 140 | // New file 141 | FileUtils.write(new File(baseDir, "src/main/java/org/dummy/Dummy2.java"), "package org.dummy;\npublic class Dummy2 {}", StandardCharsets.UTF_8, true); 142 | 143 | BuildResult result = runSonar(baseDir); 144 | 145 | String logs = result.getLogs(); 146 | assertThat(logs).contains("Missing blame information for the following files"); 147 | String files = StringUtils.substringBefore(substringAfter(logs, "Missing blame information for the following files:"), "This may lead to missing/broken features"); 148 | assertThat(files).contains("src/main/java/org/dummy/Dummy.java", 149 | "src/main/java/org/dummy/Dummy2.java"); 150 | 151 | if (orchestrator.getServer().version().isGreaterThanOrEquals(7, 1)) { 152 | assertThat(getScmData("dummy:dummy:src/main/java/org/dummy/Dummy.java")).hasSize(29); 153 | } else { 154 | assertThat(getScmData("dummy:dummy:src/main/java/org/dummy/Dummy.java")).isEmpty(); 155 | } 156 | } 157 | 158 | // SONAR-5843 159 | @Test 160 | public void sample_svn_project_with_merge() throws Exception { 161 | File repo = unzip("repo-svn-with-merge.zip"); 162 | 163 | // The "repo-svn-with-merge" repository has a "trunk" subPath => suffix should be added 164 | String tmpWkSubPath = wkSubPath; 165 | if (StringUtils.isNotBlank(tmpWkSubPath)) { 166 | tmpWkSubPath = tmpWkSubPath + "/trunk"; 167 | } 168 | String tmpBaseDirSubPath = baseDirSubPath; 169 | if (StringUtils.isNotBlank(tmpBaseDirSubPath)) { 170 | tmpBaseDirSubPath = tmpBaseDirSubPath + "/trunk"; 171 | } 172 | 173 | String scmUrl = "file:///" + unixPath(new File(repo, "repo-svn/" + tmpWkSubPath)); 174 | 175 | runSonar(new File(checkout(scmUrl), tmpBaseDirSubPath)); 176 | 177 | assertThat(getScmData("dummy:dummy:src/main/java/org/dummy/Dummy.java")) 178 | .contains( 179 | MapEntry.entry(1, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot")), 180 | MapEntry.entry(2, new LineData("6", "2014-11-06T09:23:04+0100", "henryju")), 181 | MapEntry.entry(3, new LineData("2", "2012-07-19T11:44:57+0200", "dgageot")), 182 | MapEntry.entry(24, new LineData("6", "2014-11-06T09:23:04+0100", "henryju"))); 183 | } 184 | 185 | private static String unixPath(File file) { 186 | return file.getAbsolutePath().replace('\\', '/'); 187 | } 188 | 189 | private File checkout(String scmUrl) throws Exception { 190 | ISVNOptions options = SVNWCUtil.createDefaultOptions(true); 191 | ISVNAuthenticationManager isvnAuthenticationManager = SVNWCUtil.createDefaultAuthenticationManager(null, null, (char[]) null, false); 192 | SVNClientManager svnClientManager = SVNClientManager.newInstance(options, isvnAuthenticationManager); 193 | File out = temp.newFolder(); 194 | SVNUpdateClient updateClient = svnClientManager.getUpdateClient(); 195 | SvnCheckout co = updateClient.getOperationsFactory().createCheckout(); 196 | co.setUpdateLocksOnDemand(updateClient.isUpdateLocksOnDemand()); 197 | co.setSource(SvnTarget.fromURL(SVNURL.parseURIEncoded(scmUrl), SVNRevision.HEAD)); 198 | co.setSingleTarget(SvnTarget.fromFile(out)); 199 | co.setRevision(SVNRevision.HEAD); 200 | co.setDepth(SVNDepth.INFINITY); 201 | co.setAllowUnversionedObstructions(false); 202 | co.setIgnoreExternals(updateClient.isIgnoreExternals()); 203 | co.setExternalsHandler(SvnCodec.externalsHandler(updateClient.getExternalsHandler())); 204 | co.setTargetWorkingCopyFormat(wcVersion); 205 | co.run(); 206 | return out; 207 | } 208 | 209 | public File unzip(String zipName) { 210 | try { 211 | File out = temp.newFolder(); 212 | ZipUtils.unzip(new File(new File(REPO_DIR, serverVersion), zipName), out); 213 | return out; 214 | } catch (IOException e) { 215 | throw new IllegalStateException(e); 216 | } 217 | } 218 | 219 | public static BuildResult runSonar(File baseDir, String... keyValues) throws IOException { 220 | File pom = new File(baseDir, "pom.xml"); 221 | Files.createDirectories(baseDir.toPath().resolve("target/classes")); 222 | 223 | MavenBuild sonar = MavenBuild.create(pom).setGoals("sonar:sonar"); 224 | sonar.setProperty("sonar.scm.disabled", "false"); 225 | sonar.setProperty("sonar.java.binaries", "target/classes"); 226 | sonar.setProperties(keyValues); 227 | return orchestrator.executeBuild(sonar); 228 | } 229 | 230 | private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 231 | 232 | private class LineData { 233 | 234 | final String revision; 235 | final Date date; 236 | final String author; 237 | 238 | public LineData(String revision, String datetime, String author) throws ParseException { 239 | this.revision = revision; 240 | this.date = DATETIME_FORMAT.parse(datetime); 241 | this.author = author; 242 | } 243 | 244 | @Override 245 | public boolean equals(Object obj) { 246 | return EqualsBuilder.reflectionEquals(this, obj); 247 | } 248 | 249 | @Override 250 | public int hashCode() { 251 | return new HashCodeBuilder().append(revision).append(date).append(author).toHashCode(); 252 | } 253 | 254 | @Override 255 | public String toString() { 256 | return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE); 257 | } 258 | } 259 | 260 | private Map getScmData(String fileKey) throws ParseException { 261 | 262 | Map result = new HashMap(); 263 | String json = orchestrator.getServer().adminWsClient().get("api/sources/scm", "commits_by_line", "true", "key", fileKey); 264 | JSONObject obj = (JSONObject) JSONValue.parse(json); 265 | JSONArray array = (JSONArray) obj.get("scm"); 266 | for (int i = 0; i < array.size(); i++) { 267 | JSONArray item = (JSONArray) array.get(i); 268 | // Time part was added in 5.2 269 | String dateOrDatetime = (String) item.get(2); 270 | // Revision was added in 5.2 271 | result.put(((Long) item.get(0)).intValue(), new LineData((String) item.get(3), dateOrDatetime, (String) item.get(1))); 272 | } 273 | return result; 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.sonarsource.parent 6 | parent 7 | 59.0.29 8 | 9 | org.sonarsource.scm.svn 10 | svn 11 | 1.10-SNAPSHOT 12 | pom 13 | SonarQube :: Plugins :: SCM :: SVN 14 | Subversion SCM Provider for SonarQube 15 | http://redirect.sonarsource.com/plugins/scmsvn.html 16 | 2014 17 | 18 | 19 | SonarSource 20 | http://www.sonarsource.com 21 | 22 | 23 | 24 | 25 | GNU LGPL 3 26 | http://www.gnu.org/licenses/lgpl.txt 27 | repo 28 | 29 | 30 | 31 | 32 | 33 | henryju 34 | Julien Henry 35 | +1 36 | 37 | 38 | 39 | 40 | sonar-scm-svn-plugin 41 | 42 | 43 | 44 | 45 | scm:git:git@github.com:SonarSource/sonar-scm-svn.git 46 | scm:git:git@github.com:SonarSource/sonar-scm-svn.git 47 | https://github.com/SonarSource/sonar-scm-svn 48 | HEAD 49 | 50 | 51 | 52 | jira 53 | https://jira.sonarsource.com/browse/SONARSCSVN 54 | 55 | 56 | 57 | Travis 58 | https://travis-ci.org/SonarSource/sonar-scm-svn 59 | 60 | 61 | 62 | 63 | sonar-scm-svn 64 | 65 | ${project.groupId}:sonar-scm-svn-plugin:jar 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-javadoc-plugin 74 | 75 | 8 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | its 85 | 86 | its 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.sonarsource.scm.svn 7 | svn 8 | 1.10-SNAPSHOT 9 | 10 | sonar-scm-svn-plugin 11 | sonar-plugin 12 | SonarQube :: Plugins :: SCM :: SVN 13 | Subversion SCM Provider for SonarQube 14 | http://redirect.sonarsource.com/plugins/scmsvn.html 15 | 16 | 17 | https://github.com/SonarSource/sonar-scm-svn 18 | 19 | 20 | 21 | 22 | 7.9 23 | SVN 24 | org.sonar.plugins.scm.svn.SvnPlugin 25 | 26 | 27 | 28 | 29 | com.google.code.findbugs 30 | jsr305 31 | 3.0.2 32 | provided 33 | 34 | 35 | org.sonarsource.sonarqube 36 | sonar-plugin-api 37 | ${sonar.buildVersion} 38 | provided 39 | 40 | 41 | org.tmatesoft.svnkit 42 | svnkit 43 | 1.10.1 44 | 45 | 46 | 47 | 48 | junit 49 | junit 50 | 4.13.1 51 | test 52 | 53 | 54 | org.assertj 55 | assertj-core 56 | 3.13.2 57 | test 58 | 59 | 60 | org.mockito 61 | mockito-core 62 | 3.0.0 63 | test 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.sonarsource.sonar-packaging-maven-plugin 71 | sonar-packaging-maven-plugin 72 | 1.18.0.372 73 | 74 | Svn 75 | true 76 | org.sonar.plugins.scm.svn.SvnPlugin 77 | 5.6 78 | 79 | 80 | 81 | maven-shade-plugin 82 | 83 | 84 | package 85 | 86 | shade 87 | 88 | 89 | false 90 | false 91 | false 92 | 93 | 94 | *:* 95 | 96 | META-INF/LICENSE* 97 | META-INF/NOTICE* 98 | META-INF/*.RSA 99 | META-INF/*.SF 100 | LICENSE* 101 | NOTICE* 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/AnnotationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.util.ArrayList; 24 | import java.util.Date; 25 | import java.util.List; 26 | import org.sonar.api.batch.scm.BlameLine; 27 | import org.tmatesoft.svn.core.SVNException; 28 | import org.tmatesoft.svn.core.wc.ISVNAnnotateHandler; 29 | 30 | public class AnnotationHandler implements ISVNAnnotateHandler { 31 | 32 | private List lines = new ArrayList<>(); 33 | 34 | @Override 35 | public void handleEOF() { 36 | // Not used 37 | } 38 | 39 | @Override 40 | public void handleLine(Date date, long revision, String author, String line) throws SVNException { 41 | // deprecated 42 | } 43 | 44 | @Override 45 | public void handleLine(Date date, long revision, String author, String line, Date mergedDate, 46 | long mergedRevision, String mergedAuthor, String mergedPath, int lineNumber) throws SVNException { 47 | lines.add(new BlameLine().date(mergedDate).revision(Long.toString(mergedRevision)).author(mergedAuthor)); 48 | } 49 | 50 | @Override 51 | public boolean handleRevision(Date date, long revision, String author, File contents) throws SVNException { 52 | /* 53 | * We do not want our file to be annotated for each revision of the range, but only for the last 54 | * revision of it, so we return false 55 | */ 56 | return false; 57 | } 58 | 59 | public List getLines() { 60 | return lines; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/ChangedLinesComputer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.OutputStream; 23 | import java.nio.file.Path; 24 | import java.nio.file.Paths; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | 32 | class ChangedLinesComputer { 33 | 34 | private final Tracker tracker; 35 | 36 | private final OutputStream receiver = new OutputStream() { 37 | StringBuilder sb = new StringBuilder(); 38 | 39 | @Override 40 | public void write(int b) { 41 | sb.append((char) b); 42 | if (b == '\n') { 43 | tracker.parseLine(sb.toString()); 44 | sb.setLength(0); 45 | } 46 | } 47 | }; 48 | 49 | ChangedLinesComputer(Path rootBaseDir, Set included) { 50 | this.tracker = new Tracker(rootBaseDir, included); 51 | } 52 | 53 | /** 54 | * The OutputStream to pass to svnkit's diff command. 55 | */ 56 | OutputStream receiver() { 57 | return receiver; 58 | } 59 | 60 | /** 61 | * From a stream of svn-style unified diff lines, 62 | * compute the line numbers that should be considered changed. 63 | * 64 | * Example input: 65 | *
 66 |    * Index: path/to/file
 67 |    * ===================================================================
 68 |    * --- lao 2002-02-21 23:30:39.942229878 -0800
 69 |    * +++ tzu 2002-02-21 23:30:50.442260588 -0800
 70 |    * @@ -1,7 +1,6 @@
 71 |    * -The Way that can be told of is not the eternal Way;
 72 |    * -The name that can be named is not the eternal name.
 73 |    *  The Nameless is the origin of Heaven and Earth;
 74 |    * -The Named is the mother of all things.
 75 |    * +The named is the mother of all things.
 76 |    * +
 77 |    *  Therefore let there always be non-being,
 78 |    *    so we may see their subtlety,
 79 |    *  And let there always be being,
 80 |    * @@ -9,3 +8,6 @@
 81 |    *  The two are the same,
 82 |    *  But after they are produced,
 83 |    *    they have different names.
 84 |    * +They both may be called deep and profound.
 85 |    * +Deeper and more profound,
 86 |    * +The door of all subtleties!
 87 |    * 
88 | * 89 | * See also: http://www.gnu.org/software/diffutils/manual/html_node/Example-Unified.html#Example-Unified 90 | */ 91 | Map> changedLines() { 92 | return tracker.changedLines(); 93 | } 94 | 95 | private static class Tracker { 96 | 97 | private static final Pattern START_LINE_IN_TARGET = Pattern.compile(" \\+(\\d+)"); 98 | private static final String ENTRY_START_PREFIX = "Index: "; 99 | 100 | private final Map> changedLines = new HashMap<>(); 101 | private final Set included; 102 | private final Path rootBaseDir; 103 | 104 | private int lineNumInTarget; 105 | private Path currentPath = null; 106 | private int skipCount = 0; 107 | 108 | Tracker(Path rootBaseDir, Set included) { 109 | this.rootBaseDir = rootBaseDir; 110 | this.included = included; 111 | } 112 | 113 | private void parseLine(String line) { 114 | if (line.startsWith(ENTRY_START_PREFIX)) { 115 | currentPath = Paths.get(line.substring(ENTRY_START_PREFIX.length()).trim()); 116 | if (!currentPath.isAbsolute()) { 117 | currentPath = rootBaseDir.resolve(currentPath); 118 | } 119 | if (!included.contains(currentPath)) { 120 | return; 121 | } 122 | skipCount = 3; 123 | return; 124 | } 125 | 126 | if (!included.contains(currentPath)) { 127 | return; 128 | } 129 | 130 | if (skipCount > 0) { 131 | skipCount--; 132 | return; 133 | } 134 | 135 | if (line.startsWith("@@ ")) { 136 | Matcher matcher = START_LINE_IN_TARGET.matcher(line); 137 | if (!matcher.find()) { 138 | throw new IllegalStateException("Invalid block header: " + line); 139 | } 140 | lineNumInTarget = Integer.parseInt(matcher.group(1)); 141 | return; 142 | } 143 | 144 | parseContent(line); 145 | } 146 | 147 | private void parseContent(String line) { 148 | char firstChar = line.charAt(0); 149 | if (firstChar == ' ') { 150 | lineNumInTarget++; 151 | } else if (firstChar == '+') { 152 | changedLines 153 | .computeIfAbsent(currentPath, path -> new HashSet<>()) 154 | .add(lineNumInTarget); 155 | lineNumInTarget++; 156 | } 157 | } 158 | 159 | Map> changedLines() { 160 | return changedLines; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/FindFork.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.nio.file.Path; 24 | import java.time.Instant; 25 | import java.util.Optional; 26 | import javax.annotation.CheckForNull; 27 | import org.sonar.api.scanner.ScannerSide; 28 | import org.sonar.api.utils.log.Logger; 29 | import org.sonar.api.utils.log.Loggers; 30 | import org.tmatesoft.svn.core.ISVNLogEntryHandler; 31 | import org.tmatesoft.svn.core.SVNException; 32 | import org.tmatesoft.svn.core.SVNLogEntry; 33 | import org.tmatesoft.svn.core.SVNLogEntryPath; 34 | import org.tmatesoft.svn.core.wc.SVNClientManager; 35 | import org.tmatesoft.svn.core.wc.SVNRevision; 36 | import org.tmatesoft.svn.core.wc.SVNStatus; 37 | 38 | import static org.sonar.plugins.scm.svn.SvnPlugin.newSvnClientManager; 39 | 40 | @ScannerSide 41 | public class FindFork { 42 | private static final Logger LOG = Loggers.get(FindFork.class); 43 | 44 | private final SvnConfiguration configuration; 45 | 46 | public FindFork(SvnConfiguration configuration) { 47 | this.configuration = configuration; 48 | } 49 | 50 | @CheckForNull 51 | public Instant findDate(Path location, String referenceBranch) throws SVNException { 52 | ForkPoint forkPoint = find(location, referenceBranch); 53 | if (forkPoint != null) { 54 | return forkPoint.date(); 55 | } 56 | return null; 57 | } 58 | 59 | @CheckForNull 60 | public ForkPoint find(Path location, String referenceBranch) throws SVNException { 61 | SVNClientManager clientManager = newSvnClientManager(configuration); 62 | SVNRevision revision = getSvnRevision(location, clientManager); 63 | LOG.debug("latest revision is " + revision); 64 | String svnRefBranch = "/" + referenceBranch; 65 | 66 | SVNLogEntryHolder handler = new SVNLogEntryHolder(); 67 | SVNRevision endRevision = SVNRevision.create(1); 68 | SVNRevision startRevision = SVNRevision.create(revision.getNumber()); 69 | 70 | do { 71 | clientManager.getLogClient().doLog(new File[] {location.toFile()}, startRevision, endRevision, true, true, -1, handler); 72 | SVNLogEntry lastEntry = handler.getLastEntry(); 73 | Optional copyFromReference = lastEntry.getChangedPaths().values().stream() 74 | .filter(e -> e.getCopyPath() != null && e.getCopyPath().equals(svnRefBranch)) 75 | .findFirst(); 76 | 77 | if (copyFromReference.isPresent()) { 78 | return new ForkPoint(String.valueOf(copyFromReference.get().getCopyRevision()), Instant.ofEpochMilli(lastEntry.getDate().getTime())); 79 | } 80 | 81 | if (lastEntry.getChangedPaths().isEmpty()) { 82 | // shouldn't happen since it should only stop in revisions with changed paths 83 | return null; 84 | } 85 | 86 | SVNLogEntryPath firstChangedPath = lastEntry.getChangedPaths().values().iterator().next(); 87 | if (firstChangedPath.getCopyPath() == null) { 88 | // we walked the history to the root, and the last commit found had no copy reference. Must be the trunk, there is no fork point 89 | return null; 90 | } 91 | 92 | // TODO Looks like a revision can have multiple changed paths. Should we iterate through all of them? 93 | startRevision = SVNRevision.create(firstChangedPath.getCopyRevision()); 94 | } while (true); 95 | 96 | } 97 | 98 | private SVNRevision getSvnRevision(Path location, SVNClientManager clientManager) throws SVNException { 99 | SVNStatus svnStatus = clientManager.getStatusClient().doStatus(location.toFile(), false); 100 | return svnStatus.getRevision(); 101 | } 102 | 103 | /** 104 | * Handler keeping only the last entry, and count how many entries have been seen. 105 | */ 106 | private static class SVNLogEntryHolder implements ISVNLogEntryHandler { 107 | SVNLogEntry value; 108 | 109 | public SVNLogEntry getLastEntry() { 110 | return value; 111 | } 112 | 113 | @Override 114 | public void handleLogEntry(SVNLogEntry svnLogEntry) { 115 | this.value = svnLogEntry; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/ForkPoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.time.Instant; 23 | 24 | public class ForkPoint { 25 | private String commit; 26 | private Instant date; 27 | 28 | public ForkPoint(String commit, Instant date) { 29 | this.commit = commit; 30 | this.date = date; 31 | } 32 | 33 | public String commit() { 34 | return commit; 35 | } 36 | 37 | public Instant date() { 38 | return date; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "ForkPoint{" + 44 | "commit='" + commit + '\'' + 45 | ", date=" + date + 46 | '}'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.util.List; 23 | import org.sonar.api.batch.fs.FileSystem; 24 | import org.sonar.api.batch.fs.InputFile; 25 | import org.sonar.api.batch.scm.BlameCommand; 26 | import org.sonar.api.batch.scm.BlameLine; 27 | import org.sonar.api.utils.log.Logger; 28 | import org.sonar.api.utils.log.Loggers; 29 | import org.tmatesoft.svn.core.SVNErrorCode; 30 | import org.tmatesoft.svn.core.SVNException; 31 | import org.tmatesoft.svn.core.wc.SVNClientManager; 32 | import org.tmatesoft.svn.core.wc.SVNDiffOptions; 33 | import org.tmatesoft.svn.core.wc.SVNLogClient; 34 | import org.tmatesoft.svn.core.wc.SVNRevision; 35 | import org.tmatesoft.svn.core.wc.SVNStatus; 36 | import org.tmatesoft.svn.core.wc.SVNStatusClient; 37 | import org.tmatesoft.svn.core.wc.SVNStatusType; 38 | 39 | import static org.sonar.plugins.scm.svn.SvnPlugin.newSvnClientManager; 40 | 41 | public class SvnBlameCommand extends BlameCommand { 42 | 43 | private static final Logger LOG = Loggers.get(SvnBlameCommand.class); 44 | private final SvnConfiguration configuration; 45 | 46 | public SvnBlameCommand(SvnConfiguration configuration) { 47 | this.configuration = configuration; 48 | } 49 | 50 | @Override 51 | public void blame(final BlameInput input, final BlameOutput output) { 52 | FileSystem fs = input.fileSystem(); 53 | LOG.debug("Working directory: " + fs.baseDir().getAbsolutePath()); 54 | SVNClientManager clientManager = null; 55 | try { 56 | clientManager = newSvnClientManager(configuration); 57 | for (InputFile inputFile : input.filesToBlame()) { 58 | blame(clientManager, inputFile, output); 59 | } 60 | } finally { 61 | if (clientManager != null) { 62 | try { 63 | clientManager.dispose(); 64 | } catch (Exception e) { 65 | LOG.warn("Unable to dispose SVN ClientManager", e); 66 | } 67 | } 68 | } 69 | } 70 | 71 | private static void blame(SVNClientManager clientManager, InputFile inputFile, BlameOutput output) { 72 | String filename = inputFile.relativePath(); 73 | 74 | LOG.debug("Process file {}", filename); 75 | 76 | AnnotationHandler handler = new AnnotationHandler(); 77 | try { 78 | if (!checkStatus(clientManager, inputFile)) { 79 | return; 80 | } 81 | SVNLogClient logClient = clientManager.getLogClient(); 82 | logClient.setDiffOptions(new SVNDiffOptions(true, true, true)); 83 | logClient.doAnnotate(inputFile.file(), SVNRevision.UNDEFINED, SVNRevision.create(1), SVNRevision.BASE, true, true, handler, null); 84 | } catch (SVNException e) { 85 | throw new IllegalStateException("Error when executing blame for file " + filename, e); 86 | } 87 | 88 | List lines = handler.getLines(); 89 | if (lines.size() == inputFile.lines() - 1) { 90 | // SONARPLUGINS-3097 SVN do not report blame on last empty line 91 | lines.add(lines.get(lines.size() - 1)); 92 | } 93 | output.blameResult(inputFile, lines); 94 | } 95 | 96 | private static boolean checkStatus(SVNClientManager clientManager, InputFile inputFile) throws SVNException { 97 | SVNStatusClient statusClient = clientManager.getStatusClient(); 98 | try { 99 | SVNStatus status = statusClient.doStatus(inputFile.file(), false); 100 | if (status == null) { 101 | LOG.debug("File {} returns no svn state. Skipping it.", inputFile); 102 | return false; 103 | } 104 | if (status.getContentsStatus() != SVNStatusType.STATUS_NORMAL) { 105 | LOG.debug("File {} is not versionned or contains local modifications. Skipping it.", inputFile); 106 | return false; 107 | } 108 | } catch (SVNException e) { 109 | if (SVNErrorCode.WC_PATH_NOT_FOUND.equals(e.getErrorMessage().getErrorCode()) 110 | || SVNErrorCode.WC_NOT_WORKING_COPY.equals(e.getErrorMessage().getErrorCode())) { 111 | LOG.debug("File {} is not versionned. Skipping it.", inputFile); 112 | return false; 113 | } 114 | throw e; 115 | } 116 | return true; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import javax.annotation.CheckForNull; 27 | import org.sonar.api.CoreProperties; 28 | import org.sonar.api.PropertyType; 29 | import org.sonar.api.config.Configuration; 30 | import org.sonar.api.config.PropertyDefinition; 31 | import org.sonar.api.resources.Qualifiers; 32 | import org.sonar.api.scanner.ScannerSide; 33 | import org.sonar.api.utils.MessageException; 34 | 35 | @ScannerSide 36 | public class SvnConfiguration { 37 | 38 | private static final String CATEGORY_SVN = "SVN"; 39 | public static final String USER_PROP_KEY = "sonar.svn.username"; 40 | public static final String PRIVATE_KEY_PATH_PROP_KEY = "sonar.svn.privateKeyPath"; 41 | public static final String PASSWORD_PROP_KEY = "sonar.svn.password.secured"; 42 | public static final String PASSPHRASE_PROP_KEY = "sonar.svn.passphrase.secured"; 43 | private final Configuration config; 44 | 45 | public SvnConfiguration(Configuration config) { 46 | this.config = config; 47 | } 48 | 49 | public static List getProperties() { 50 | return Arrays.asList( 51 | PropertyDefinition.builder(USER_PROP_KEY) 52 | .name("Username") 53 | .description("Username to be used for SVN server or SVN+SSH authentication") 54 | .type(PropertyType.STRING) 55 | .onQualifiers(Qualifiers.PROJECT) 56 | .category(CoreProperties.CATEGORY_SCM) 57 | .subCategory(CATEGORY_SVN) 58 | .index(0) 59 | .build(), 60 | PropertyDefinition.builder(PASSWORD_PROP_KEY) 61 | .name("Password") 62 | .description("Password to be used for SVN server or SVN+SSH authentication") 63 | .type(PropertyType.PASSWORD) 64 | .onQualifiers(Qualifiers.PROJECT) 65 | .category(CoreProperties.CATEGORY_SCM) 66 | .subCategory(CATEGORY_SVN) 67 | .index(1) 68 | .build(), 69 | PropertyDefinition.builder(PRIVATE_KEY_PATH_PROP_KEY) 70 | .name("Path to private key file") 71 | .description("Can be used instead of password for SVN+SSH authentication") 72 | .type(PropertyType.STRING) 73 | .onQualifiers(Qualifiers.PROJECT) 74 | .category(CoreProperties.CATEGORY_SCM) 75 | .subCategory(CATEGORY_SVN) 76 | .index(2) 77 | .build(), 78 | PropertyDefinition.builder(PASSPHRASE_PROP_KEY) 79 | .name("Passphrase") 80 | .description("Optional passphrase of your private key file") 81 | .type(PropertyType.PASSWORD) 82 | .onQualifiers(Qualifiers.PROJECT) 83 | .category(CoreProperties.CATEGORY_SCM) 84 | .subCategory(CATEGORY_SVN) 85 | .index(3) 86 | .build()); 87 | } 88 | 89 | @CheckForNull 90 | public String username() { 91 | return config.get(USER_PROP_KEY).orElse(null); 92 | } 93 | 94 | @CheckForNull 95 | public String password() { 96 | return config.get(PASSWORD_PROP_KEY).orElse(null); 97 | } 98 | 99 | @CheckForNull 100 | public File privateKey() { 101 | Optional privateKeyOpt = config.get(PRIVATE_KEY_PATH_PROP_KEY); 102 | if (privateKeyOpt.isPresent()) { 103 | File privateKeyFile = new File(privateKeyOpt.get()); 104 | if (!privateKeyFile.exists() || !privateKeyFile.isFile() || !privateKeyFile.canRead()) { 105 | throw MessageException.of("Unable to read private key from '" + privateKeyFile + "'"); 106 | } 107 | return privateKeyFile; 108 | } 109 | return null; 110 | } 111 | 112 | @CheckForNull 113 | public String passPhrase() { 114 | return config.get(PASSPHRASE_PROP_KEY).orElse(null); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import javax.annotation.CheckForNull; 23 | import javax.annotation.Nullable; 24 | import org.sonar.api.Plugin; 25 | import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 26 | import org.tmatesoft.svn.core.wc.ISVNOptions; 27 | import org.tmatesoft.svn.core.wc.SVNClientManager; 28 | import org.tmatesoft.svn.core.wc.SVNWCUtil; 29 | 30 | public final class SvnPlugin implements Plugin { 31 | static SVNClientManager newSvnClientManager(SvnConfiguration configuration) { 32 | ISVNOptions options = SVNWCUtil.createDefaultOptions(true); 33 | final char[] passwordValue = getCharsOrNull(configuration.password()); 34 | final char[] passPhraseValue = getCharsOrNull(configuration.passPhrase()); 35 | ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager( 36 | null, 37 | configuration.username(), 38 | passwordValue, 39 | configuration.privateKey(), 40 | passPhraseValue, 41 | false); 42 | return SVNClientManager.newInstance(options, authManager); 43 | } 44 | 45 | @CheckForNull 46 | private static char[] getCharsOrNull(@Nullable String s) { 47 | return s != null ? s.toCharArray() : null; 48 | } 49 | 50 | @Override 51 | public void define(Context context) { 52 | context.addExtensions(SvnScmProvider.class, 53 | SvnBlameCommand.class, 54 | SvnConfiguration.class, 55 | FindFork.class); 56 | context.addExtensions(SvnConfiguration.getProperties()); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.net.MalformedURLException; 24 | import java.net.URISyntaxException; 25 | import java.net.URL; 26 | import java.nio.file.Path; 27 | import java.nio.file.Paths; 28 | import java.time.Instant; 29 | import java.util.HashSet; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import javax.annotation.CheckForNull; 33 | import org.sonar.api.batch.scm.BlameCommand; 34 | import org.sonar.api.batch.scm.ScmProvider; 35 | import org.sonar.api.utils.log.Logger; 36 | import org.sonar.api.utils.log.Loggers; 37 | import org.tmatesoft.svn.core.SVNDepth; 38 | import org.tmatesoft.svn.core.SVNException; 39 | import org.tmatesoft.svn.core.SVNLogEntryPath; 40 | import org.tmatesoft.svn.core.SVNNodeKind; 41 | import org.tmatesoft.svn.core.SVNURL; 42 | import org.tmatesoft.svn.core.wc.SVNClientManager; 43 | import org.tmatesoft.svn.core.wc.SVNDiffClient; 44 | import org.tmatesoft.svn.core.wc.SVNInfo; 45 | import org.tmatesoft.svn.core.wc.SVNLogClient; 46 | import org.tmatesoft.svn.core.wc.SVNRevision; 47 | import org.tmatesoft.svn.core.wc.SVNWCClient; 48 | 49 | import static org.sonar.plugins.scm.svn.SvnPlugin.newSvnClientManager; 50 | 51 | public class SvnScmProvider extends ScmProvider { 52 | 53 | private static final Logger LOG = Loggers.get(SvnScmProvider.class); 54 | 55 | private final SvnConfiguration configuration; 56 | private final SvnBlameCommand blameCommand; 57 | private final FindFork findFork; 58 | 59 | public SvnScmProvider(SvnConfiguration configuration, SvnBlameCommand blameCommand, FindFork findFork) { 60 | this.configuration = configuration; 61 | this.blameCommand = blameCommand; 62 | this.findFork = findFork; 63 | } 64 | 65 | @Override 66 | public String key() { 67 | return "svn"; 68 | } 69 | 70 | @Override 71 | public boolean supports(File baseDir) { 72 | File folder = baseDir; 73 | while (folder != null) { 74 | if (new File(folder, ".svn").exists()) { 75 | return true; 76 | } 77 | folder = folder.getParentFile(); 78 | } 79 | return false; 80 | } 81 | 82 | @Override 83 | public BlameCommand blameCommand() { 84 | return blameCommand; 85 | } 86 | 87 | @CheckForNull 88 | @Override 89 | public Set branchChangedFiles(String targetBranchName, Path rootBaseDir) { 90 | SVNClientManager clientManager = null; 91 | try { 92 | clientManager = newSvnClientManager(configuration); 93 | return computeChangedPaths(rootBaseDir, clientManager); 94 | } catch (SVNException e) { 95 | LOG.warn(e.getMessage()); 96 | } finally { 97 | if (clientManager != null) { 98 | try { 99 | clientManager.dispose(); 100 | } catch (Exception e) { 101 | LOG.warn("Unable to dispose SVN ClientManager", e); 102 | } 103 | } 104 | } 105 | 106 | return null; 107 | } 108 | 109 | static Set computeChangedPaths(Path projectBasedir, SVNClientManager clientManager) throws SVNException { 110 | SVNWCClient wcClient = clientManager.getWCClient(); 111 | SVNInfo svnInfo = wcClient.doInfo(projectBasedir.toFile(), null); 112 | 113 | // SVN path of the repo root, for example: /C:/Users/JANOSG~1/AppData/Local/Temp/x/y 114 | Path svnRootPath = toPath(svnInfo.getRepositoryRootURL()); 115 | 116 | // the svn root path may be "" for urls like http://svnserver/ 117 | // -> set it to "/" to avoid crashing when using Path.relativize later 118 | if (svnRootPath.equals(Paths.get(""))) { 119 | svnRootPath = Paths.get("/"); 120 | } 121 | 122 | // SVN path of projectBasedir, for example: /C:/Users/JANOSG~1/AppData/Local/Temp/x/y/branches/b1 123 | Path svnProjectPath = toPath(svnInfo.getURL()); 124 | // path of projectBasedir, as "absolute path within the SVN repo", for example: /branches/b1 125 | Path inRepoProjectPath = Paths.get("/").resolve(svnRootPath.relativize(svnProjectPath)); 126 | 127 | // We inspect "svn log" from latest revision until copy-point. 128 | // The same path may appear in multiple commits, the ordering of changes and removals is important. 129 | Set paths = new HashSet<>(); 130 | Set removed = new HashSet<>(); 131 | 132 | SVNLogClient svnLogClient = clientManager.getLogClient(); 133 | svnLogClient.doLog(new File[] {projectBasedir.toFile()}, null, null, null, true, true, 0, svnLogEntry -> { 134 | svnLogEntry.getChangedPaths().values().forEach(entry -> { 135 | if (entry.getKind().equals(SVNNodeKind.FILE)) { 136 | Path path = projectBasedir.resolve(inRepoProjectPath.relativize(Paths.get(entry.getPath()))); 137 | if (isModified(entry)) { 138 | // Skip if the path is removed in a more recent commit 139 | if (!removed.contains(path)) { 140 | paths.add(path); 141 | } 142 | } else if (entry.getType() == SVNLogEntryPath.TYPE_DELETED) { 143 | removed.add(path); 144 | } 145 | } 146 | }); 147 | }); 148 | return paths; 149 | } 150 | 151 | private static Path toPath(SVNURL svnUrl) { 152 | if ("file".equals(svnUrl.getProtocol())) { 153 | try { 154 | return Paths.get(new URL("file", svnUrl.getHost(), svnUrl.getPath()).toURI()); 155 | } catch (URISyntaxException | MalformedURLException e) { 156 | throw new IllegalStateException(e); 157 | } 158 | } 159 | return Paths.get(svnUrl.getURIEncodedPath()); 160 | } 161 | 162 | private static boolean isModified(SVNLogEntryPath entry) { 163 | return entry.getType() == SVNLogEntryPath.TYPE_ADDED 164 | || entry.getType() == SVNLogEntryPath.TYPE_MODIFIED; 165 | } 166 | 167 | @CheckForNull 168 | @Override 169 | public Map> branchChangedLines(String targetBranchName, Path rootBaseDir, Set changedFiles) { 170 | SVNClientManager clientManager = null; 171 | try { 172 | clientManager = newSvnClientManager(configuration); 173 | 174 | // find reference revision number: the copy point 175 | SVNLogClient svnLogClient = clientManager.getLogClient(); 176 | long[] revisionCounter = {0}; 177 | svnLogClient.doLog(new File[] {rootBaseDir.toFile()}, null, null, null, true, true, 0, 178 | svnLogEntry -> revisionCounter[0] = svnLogEntry.getRevision()); 179 | 180 | long startRev = revisionCounter[0]; 181 | 182 | SVNDiffClient svnDiffClient = clientManager.getDiffClient(); 183 | File path = rootBaseDir.toFile(); 184 | ChangedLinesComputer computer = newChangedLinesComputer(rootBaseDir, changedFiles); 185 | svnDiffClient.doDiff(path, SVNRevision.create(startRev), path, SVNRevision.WORKING, SVNDepth.INFINITY, false, computer.receiver(), null); 186 | return computer.changedLines(); 187 | } catch (Exception e) { 188 | LOG.warn("Failed to get changed lines from Subversion", e); 189 | } finally { 190 | if (clientManager != null) { 191 | try { 192 | clientManager.dispose(); 193 | } catch (Exception e) { 194 | LOG.warn("Unable to dispose SVN ClientManager", e); 195 | } 196 | } 197 | } 198 | 199 | return null; 200 | } 201 | 202 | /** 203 | * It will override API in 8.4 204 | */ 205 | @CheckForNull 206 | public Instant forkDate(Path rootBaseDir, String referenceBranch) { 207 | try { 208 | return findFork.findDate(rootBaseDir, referenceBranch); 209 | } catch (SVNException e) { 210 | LOG.warn("Unable to find fork date with '" + referenceBranch + "'", e); 211 | return null; 212 | } 213 | } 214 | 215 | ChangedLinesComputer newChangedLinesComputer(Path rootBaseDir, Set changedFiles) { 216 | return new ChangedLinesComputer(rootBaseDir, changedFiles); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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 | @ParametersAreNonnullByDefault 21 | package org.sonar.plugins.scm.svn; 22 | 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | 25 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/ChangedLinesComputerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.IOException; 23 | import java.io.OutputStreamWriter; 24 | import java.nio.file.Path; 25 | import java.nio.file.Paths; 26 | import org.junit.Test; 27 | import org.sonar.api.internal.google.common.collect.ImmutableMap; 28 | import org.sonar.api.internal.google.common.collect.ImmutableSet; 29 | 30 | import static java.util.Collections.singleton; 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | 33 | public class ChangedLinesComputerTest { 34 | 35 | private final Path rootBaseDir = Paths.get("/foo"); 36 | private final ChangedLinesComputer underTest = new ChangedLinesComputer(rootBaseDir, ImmutableSet.of( 37 | rootBaseDir.resolve("sample1"), 38 | rootBaseDir.resolve("sample2"), 39 | rootBaseDir.resolve("sample3"), 40 | rootBaseDir.resolve("sample4"))); 41 | 42 | @Test 43 | public void do_not_count_deleted_line() throws IOException { 44 | String example = "Index: sample1\n" 45 | + "===================================================================\n" 46 | + "--- a/sample1\n" 47 | + "+++ b/sample1\n" 48 | + "@@ -1 +0,0 @@\n" 49 | + "-deleted line\n"; 50 | 51 | printDiff(example); 52 | assertThat(underTest.changedLines()).isEmpty(); 53 | } 54 | 55 | @Test 56 | public void count_single_added_line() throws IOException { 57 | String example = "Index: sample1\n" 58 | + "===================================================================\n" 59 | + "--- a/sample1\n" 60 | + "+++ b/sample1\n" 61 | + "@@ -0,0 +1 @@\n" 62 | + "+added line\n"; 63 | 64 | printDiff(example); 65 | assertThat(underTest.changedLines()).isEqualTo(ImmutableMap.of(rootBaseDir.resolve("sample1"), singleton(1))); 66 | } 67 | 68 | @Test 69 | public void count_multiple_added_lines() throws IOException { 70 | String example = "Index: sample1\n" 71 | + "===================================================================\n" 72 | + "--- a/sample1\n" 73 | + "+++ b/sample1\n" 74 | + "@@ -1 +1,3 @@\n" 75 | + " same line\n" 76 | + "+added line 1\n" 77 | + "+added line 2\n"; 78 | 79 | printDiff(example); 80 | assertThat(underTest.changedLines()).isEqualTo(ImmutableMap.of(rootBaseDir.resolve("sample1"), ImmutableSet.of(2, 3))); 81 | } 82 | 83 | @Test 84 | public void handle_index_using_absolute_paths() throws IOException { 85 | String example = "Index: /foo/sample1\n" 86 | + "===================================================================\n" 87 | + "--- a/sample1\n" 88 | + "+++ b/sample1\n" 89 | + "@@ -1 +1,3 @@\n" 90 | + " same line\n" 91 | + "+added line 1\n" 92 | + "+added line 2\n"; 93 | 94 | printDiff(example); 95 | assertThat(underTest.changedLines()).isEqualTo(ImmutableMap.of(rootBaseDir.resolve("sample1"), ImmutableSet.of(2, 3))); 96 | } 97 | 98 | @Test 99 | public void compute_from_multiple_hunks() throws IOException { 100 | String example = "Index: sample1\n" 101 | + "===================================================================\n" 102 | + "--- lao\t2002-02-21 23:30:39.942229878 -0800\n" 103 | + "+++ tzu\t2002-02-21 23:30:50.442260588 -0800\n" 104 | + "@@ -1,7 +1,6 @@\n" 105 | + "-The Way that can be told of is not the eternal Way;\n" 106 | + "-The name that can be named is not the eternal name.\n" 107 | + " The Nameless is the origin of Heaven and Earth;\n" 108 | + "-The Named is the mother of all things.\n" 109 | + "+The named is the mother of all things.\n" 110 | + "+\n" 111 | + " Therefore let there always be non-being,\n" 112 | + " so we may see their subtlety,\n" 113 | + " And let there always be being,\n" 114 | + "@@ -9,3 +8,6 @@\n" 115 | + " The two are the same,\n" 116 | + " But after they are produced,\n" 117 | + " they have different names.\n" 118 | + "+They both may be called deep and profound.\n" 119 | + "+Deeper and more profound,\n" 120 | + "+The door of all subtleties!\n"; 121 | printDiff(example); 122 | assertThat(underTest.changedLines()).isEqualTo(ImmutableMap.of(rootBaseDir.resolve("sample1"), ImmutableSet.of(2, 3, 11, 12, 13))); 123 | } 124 | 125 | @Test(expected = IllegalStateException.class) 126 | public void crash_on_invalid_start_line_format() throws IOException { 127 | String example = "Index: sample1\n" 128 | + "===================================================================\n" 129 | + "--- a/sample1\n" 130 | + "+++ b/sample1\n" 131 | + "@@ -1 +x1,3 @@\n" 132 | + " same line\n" 133 | + "+added line 1\n" 134 | + "+added line 2\n"; 135 | 136 | printDiff(example); 137 | underTest.changedLines(); 138 | } 139 | 140 | @Test 141 | public void parse_diff_with_multiple_files() throws IOException { 142 | String example = "Index: sample1\n" 143 | + "===================================================================\n" 144 | + "--- a/sample1\n" 145 | + "+++ b/sample1\n" 146 | + "@@ -1 +0,0 @@\n" 147 | + "-deleted line\n" 148 | + "Index: sample2\n" 149 | + "===================================================================\n" 150 | + "--- a/sample2\n" 151 | + "+++ b/sample2\n" 152 | + "@@ -0,0 +1 @@\n" 153 | + "+added line\n" 154 | + "Index: sample3\n" 155 | + "===================================================================\n" 156 | + "--- a/sample3\n" 157 | + "+++ b/sample3\n" 158 | + "@@ -0,0 +1,2 @@\n" 159 | + "+added line 1\n" 160 | + "+added line 2\n" 161 | + "Index: sample3-not-included\n" 162 | + "===================================================================\n" 163 | + "--- a/sample3-not-included\n" 164 | + "+++ b/sample3-not-included\n" 165 | + "@@ -0,0 +1,2 @@\n" 166 | + "+added line 1\n" 167 | + "+added line 2\n" 168 | + "Index: sample4\n" 169 | + "===================================================================\n" 170 | + "--- a/sample4\n" 171 | + "+++ b/sample4\n" 172 | + "@@ -1 +1,3 @@\n" 173 | + " same line\n" 174 | + "+added line 1\n" 175 | + "+added line 2\n"; 176 | 177 | printDiff(example); 178 | assertThat(underTest.changedLines()) 179 | .isEqualTo( 180 | ImmutableMap.of( 181 | rootBaseDir.resolve("sample2"), ImmutableSet.of(1), 182 | rootBaseDir.resolve("sample3"), ImmutableSet.of(1, 2), 183 | rootBaseDir.resolve("sample4"), ImmutableSet.of(2, 3))); 184 | } 185 | 186 | private void printDiff(String unifiedDiff) throws IOException { 187 | try (OutputStreamWriter writer = new OutputStreamWriter(underTest.receiver())) { 188 | writer.write(unifiedDiff); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/FindForkTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import java.nio.file.Paths; 25 | import org.assertj.core.api.Assertions; 26 | import org.junit.Before; 27 | import org.junit.BeforeClass; 28 | import org.junit.ClassRule; 29 | import org.junit.Test; 30 | import org.junit.rules.TemporaryFolder; 31 | import org.mockito.Mockito; 32 | import org.mockito.internal.matchers.Find; 33 | import org.tmatesoft.svn.core.SVNException; 34 | 35 | import static org.assertj.core.api.Assertions.assertThat; 36 | import static org.mockito.Mockito.mock; 37 | 38 | public class FindForkTest { 39 | 40 | @ClassRule 41 | public static TemporaryFolder temp = new TemporaryFolder(); 42 | 43 | private static SvnTester svnTester; 44 | 45 | private static Path trunk; 46 | private static Path b1; 47 | private static Path b2; 48 | 49 | private FindFork findFork; 50 | 51 | @BeforeClass 52 | public static void before() throws IOException, SVNException { 53 | svnTester = new SvnTester(temp.newFolder().toPath()); 54 | 55 | trunk = temp.newFolder("trunk").toPath(); 56 | svnTester.checkout(trunk, "trunk"); 57 | createAndCommitFile(trunk, "file-1-commit-in-trunk.xoo"); 58 | createAndCommitFile(trunk, "file-2-commit-in-trunk.xoo"); 59 | createAndCommitFile(trunk, "file-3-commit-in-trunk.xoo"); 60 | svnTester.checkout(trunk, "trunk"); 61 | 62 | svnTester.createBranch("b1"); 63 | b1 = temp.newFolder("branches", "b1").toPath(); 64 | svnTester.checkout(b1, "branches/b1"); 65 | createAndCommitFile(b1, "file-1-commit-in-b1.xoo"); 66 | createAndCommitFile(b1, "file-2-commit-in-b1.xoo"); 67 | createAndCommitFile(b1, "file-3-commit-in-b1.xoo"); 68 | svnTester.checkout(b1, "branches/b1"); 69 | 70 | svnTester.createBranch("branches/b1", "b2"); 71 | b2 = temp.newFolder("branches", "b2").toPath(); 72 | svnTester.checkout(b2, "branches/b2"); 73 | 74 | createAndCommitFile(b2, "file-1-commit-in-b2.xoo"); 75 | createAndCommitFile(b2, "file-2-commit-in-b2.xoo"); 76 | createAndCommitFile(b2, "file-3-commit-in-b2.xoo"); 77 | svnTester.checkout(b2, "branches/b2"); 78 | } 79 | 80 | @Before 81 | public void setUp() { 82 | SvnConfiguration configurationMock = mock(SvnConfiguration.class); 83 | findFork = new FindFork(configurationMock); 84 | } 85 | 86 | @Test 87 | public void testEmptyBranch() throws SVNException, IOException { 88 | svnTester.createBranch("empty"); 89 | Path empty = temp.newFolder("branches", "empty").toPath(); 90 | 91 | svnTester.checkout(empty, "branches/empty"); 92 | ForkPoint forkPoint = findFork.find(empty, "unknown"); 93 | assertThat(forkPoint).isNull(); 94 | } 95 | 96 | @Test 97 | public void returnNoDate() throws SVNException { 98 | FindFork findFork = new FindFork(mock(SvnConfiguration.class)) { 99 | @Override 100 | public ForkPoint find(Path location, String referenceBranch) { 101 | return null; 102 | } 103 | }; 104 | 105 | assertThat(findFork.findDate(Paths.get(""), "branch")).isNull(); 106 | } 107 | 108 | @Test 109 | public void testTrunk() throws SVNException { 110 | ForkPoint forkPoint = findFork.find(trunk, "unknown"); 111 | assertThat(forkPoint).isNull(); 112 | } 113 | 114 | @Test 115 | public void testB1() throws SVNException { 116 | ForkPoint forkPoint = findFork.find(b1, "trunk"); 117 | assertThat(forkPoint.commit()).isEqualTo("5"); 118 | } 119 | 120 | @Test 121 | public void testB2() throws SVNException { 122 | ForkPoint forkPoint = findFork.find(b2, "branches/b1"); 123 | assertThat(forkPoint.commit()).isEqualTo("9"); 124 | } 125 | 126 | @Test 127 | public void testB2Date() throws SVNException { 128 | assertThat(findFork.findDate(b2, "branches/b1")).isNotNull(); 129 | } 130 | 131 | @Test 132 | public void testB2FromTrunk() throws SVNException { 133 | ForkPoint forkPoint = findFork.find(b2, "trunk"); 134 | assertThat(forkPoint.commit()).isEqualTo("5"); 135 | } 136 | 137 | private static void createAndCommitFile(Path worktree, String filename, String content) throws IOException, SVNException { 138 | svnTester.createFile(worktree, filename, content); 139 | svnTester.add(worktree, filename); 140 | svnTester.commit(worktree); 141 | } 142 | 143 | private static void createAndCommitFile(Path worktree, String filename) throws IOException, SVNException { 144 | createAndCommitFile(worktree, filename, filename + "\n"); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 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.util.Arrays; 29 | import java.util.Date; 30 | import java.util.Enumeration; 31 | import java.util.List; 32 | import java.util.zip.ZipEntry; 33 | import java.util.zip.ZipFile; 34 | import org.junit.Before; 35 | import org.junit.Rule; 36 | import org.junit.Test; 37 | import org.junit.rules.ExpectedException; 38 | import org.junit.rules.TemporaryFolder; 39 | import org.junit.runner.RunWith; 40 | import org.junit.runners.Parameterized; 41 | import org.junit.runners.Parameterized.Parameters; 42 | import org.mockito.ArgumentCaptor; 43 | import org.sonar.api.batch.fs.FileSystem; 44 | import org.sonar.api.batch.fs.InputFile; 45 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 46 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 47 | import org.sonar.api.batch.scm.BlameCommand.BlameInput; 48 | import org.sonar.api.batch.scm.BlameCommand.BlameOutput; 49 | import org.sonar.api.batch.scm.BlameLine; 50 | import org.tmatesoft.svn.core.SVNDepth; 51 | import org.tmatesoft.svn.core.SVNURL; 52 | import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 53 | import org.tmatesoft.svn.core.internal.wc2.compat.SvnCodec; 54 | import org.tmatesoft.svn.core.wc.ISVNOptions; 55 | import org.tmatesoft.svn.core.wc.SVNClientManager; 56 | import org.tmatesoft.svn.core.wc.SVNRevision; 57 | import org.tmatesoft.svn.core.wc.SVNUpdateClient; 58 | import org.tmatesoft.svn.core.wc.SVNWCUtil; 59 | import org.tmatesoft.svn.core.wc2.SvnCheckout; 60 | import org.tmatesoft.svn.core.wc2.SvnTarget; 61 | 62 | import static org.assertj.core.api.Assertions.assertThat; 63 | import static org.mockito.Matchers.eq; 64 | import static org.mockito.Mockito.mock; 65 | import static org.mockito.Mockito.verify; 66 | import static org.mockito.Mockito.verifyZeroInteractions; 67 | import static org.mockito.Mockito.when; 68 | 69 | @RunWith(Parameterized.class) 70 | public class SvnBlameCommandTest { 71 | 72 | /* 73 | * Note about SONARSCSVN-11: The case of a project baseDir is in a subFolder of working copy is part of method tests by default 74 | */ 75 | 76 | private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java"; 77 | 78 | @Rule 79 | public TemporaryFolder temp = new TemporaryFolder(); 80 | 81 | @Rule 82 | public ExpectedException thrown = ExpectedException.none(); 83 | 84 | private FileSystem fs; 85 | private BlameInput input; 86 | private String serverVersion; 87 | private int wcVersion; 88 | 89 | @Parameters(name = "SVN server version {0}, WC version {1}") 90 | public static Iterable data() { 91 | return Arrays.asList(new Object[][] {{"1.6", 10}, {"1.7", 29}, {"1.8", 31}, {"1.9", 31}}); 92 | } 93 | 94 | public SvnBlameCommandTest(String serverVersion, int wcVersion) { 95 | this.serverVersion = serverVersion; 96 | this.wcVersion = wcVersion; 97 | } 98 | 99 | @Before 100 | public void prepare() throws IOException { 101 | fs = mock(FileSystem.class); 102 | input = mock(BlameInput.class); 103 | when(input.fileSystem()).thenReturn(fs); 104 | } 105 | 106 | @Test 107 | public void testParsingOfOutput() throws Exception { 108 | File repoDir = unzip("repo-svn.zip"); 109 | 110 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 111 | File baseDir = new File(checkout(scmUrl), "dummy-svn"); 112 | 113 | when(fs.baseDir()).thenReturn(baseDir); 114 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) 115 | .setLines(27) 116 | .setModuleBaseDir(baseDir.toPath()) 117 | .build(); 118 | 119 | BlameOutput blameResult = mock(BlameOutput.class); 120 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 121 | 122 | newSvnBlameCommand().blame(input, blameResult); 123 | ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); 124 | verify(blameResult).blameResult(eq(inputFile), captor.capture()); 125 | List result = captor.getValue(); 126 | assertThat(result).hasSize(27); 127 | Date commitDate = new Date(1342691097393L); 128 | assertThat(result).containsExactly( 129 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 130 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 131 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 132 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 133 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 134 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 135 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 136 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 137 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 138 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 139 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 140 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 141 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 142 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 143 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 144 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 145 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 146 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 147 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 148 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 149 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 150 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 151 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 152 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 153 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 154 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 155 | new BlameLine().date(commitDate).revision("2").author("dgageot")); 156 | } 157 | 158 | private File unzip(String repoName) throws IOException { 159 | File repoDir = temp.newFolder(); 160 | javaUnzip(Paths.get("test-repos", serverVersion, repoName).toFile(), repoDir); 161 | return repoDir; 162 | } 163 | 164 | private File checkout(String scmUrl) throws Exception { 165 | ISVNOptions options = SVNWCUtil.createDefaultOptions(true); 166 | ISVNAuthenticationManager isvnAuthenticationManager = SVNWCUtil.createDefaultAuthenticationManager(null, null, (char[]) null, false); 167 | SVNClientManager svnClientManager = SVNClientManager.newInstance(options, isvnAuthenticationManager); 168 | File out = temp.newFolder(); 169 | SVNUpdateClient updateClient = svnClientManager.getUpdateClient(); 170 | SvnCheckout co = updateClient.getOperationsFactory().createCheckout(); 171 | co.setUpdateLocksOnDemand(updateClient.isUpdateLocksOnDemand()); 172 | co.setSource(SvnTarget.fromURL(SVNURL.parseURIEncoded(scmUrl), SVNRevision.HEAD)); 173 | co.setSingleTarget(SvnTarget.fromFile(out)); 174 | co.setRevision(SVNRevision.HEAD); 175 | co.setDepth(SVNDepth.INFINITY); 176 | co.setAllowUnversionedObstructions(false); 177 | co.setIgnoreExternals(updateClient.isIgnoreExternals()); 178 | co.setExternalsHandler(SvnCodec.externalsHandler(updateClient.getExternalsHandler())); 179 | co.setTargetWorkingCopyFormat(wcVersion); 180 | co.run(); 181 | return out; 182 | } 183 | 184 | @Test 185 | public void testParsingOfOutputWithMergeHistory() throws Exception { 186 | File repoDir = unzip("repo-svn-with-merge.zip"); 187 | 188 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 189 | File baseDir = new File(checkout(scmUrl), "dummy-svn/trunk"); 190 | 191 | when(fs.baseDir()).thenReturn(baseDir); 192 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) 193 | .setLines(27) 194 | .setModuleBaseDir(baseDir.toPath()) 195 | .build(); 196 | 197 | BlameOutput blameResult = mock(BlameOutput.class); 198 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 199 | 200 | newSvnBlameCommand().blame(input, blameResult); 201 | ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); 202 | verify(blameResult).blameResult(eq(inputFile), captor.capture()); 203 | List result = captor.getValue(); 204 | assertThat(result).hasSize(27); 205 | Date commitDate = new Date(1342691097393L); 206 | Date revision6Date = new Date(1415262184300L); 207 | assertThat(result).containsExactly( 208 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 209 | new BlameLine().date(revision6Date).revision("6").author("henryju"), 210 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 211 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 212 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 213 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 214 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 215 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 216 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 217 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 218 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 219 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 220 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 221 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 222 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 223 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 224 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 225 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 226 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 227 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 228 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 229 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 230 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 231 | new BlameLine().date(revision6Date).revision("6").author("henryju"), 232 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 233 | new BlameLine().date(commitDate).revision("2").author("dgageot"), 234 | new BlameLine().date(commitDate).revision("2").author("dgageot")); 235 | } 236 | 237 | @Test 238 | public void shouldNotFailIfFileContainsLocalModification() throws Exception { 239 | File repoDir = unzip("repo-svn.zip"); 240 | 241 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 242 | File baseDir = new File(checkout(scmUrl), "dummy-svn"); 243 | 244 | when(fs.baseDir()).thenReturn(baseDir); 245 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) 246 | .setLines(28) 247 | .setModuleBaseDir(baseDir.toPath()) 248 | .build(); 249 | 250 | Files.write(baseDir.toPath().resolve(DUMMY_JAVA), "\n//foo".getBytes(), StandardOpenOption.APPEND); 251 | 252 | BlameOutput blameResult = mock(BlameOutput.class); 253 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 254 | 255 | newSvnBlameCommand().blame(input, blameResult); 256 | verifyZeroInteractions(blameResult); 257 | } 258 | 259 | // SONARSCSVN-7 260 | @Test 261 | public void shouldNotFailOnWrongFilename() throws Exception { 262 | File repoDir = unzip("repo-svn.zip"); 263 | 264 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 265 | File baseDir = new File(checkout(scmUrl), "dummy-svn"); 266 | 267 | when(fs.baseDir()).thenReturn(baseDir); 268 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA.toLowerCase()) 269 | .setLines(27) 270 | .setModuleBaseDir(baseDir.toPath()) 271 | .build(); 272 | 273 | BlameOutput blameResult = mock(BlameOutput.class); 274 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 275 | 276 | newSvnBlameCommand().blame(input, blameResult); 277 | verifyZeroInteractions(blameResult); 278 | } 279 | 280 | @Test 281 | public void shouldNotFailOnUncommitedFile() throws Exception { 282 | File repoDir = unzip("repo-svn.zip"); 283 | 284 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 285 | File baseDir = new File(checkout(scmUrl), "dummy-svn"); 286 | 287 | when(fs.baseDir()).thenReturn(baseDir); 288 | String relativePath = "src/main/java/org/dummy/Dummy2.java"; 289 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath) 290 | .setLines(28) 291 | .setModuleBaseDir(baseDir.toPath()) 292 | .build(); 293 | 294 | Files.write(baseDir.toPath().resolve(relativePath), "package org.dummy;\npublic class Dummy2 {}".getBytes()); 295 | 296 | BlameOutput blameResult = mock(BlameOutput.class); 297 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 298 | 299 | newSvnBlameCommand().blame(input, blameResult); 300 | verifyZeroInteractions(blameResult); 301 | } 302 | 303 | @Test 304 | public void shouldNotFailOnUncommitedDir() throws Exception { 305 | File repoDir = unzip("repo-svn.zip"); 306 | 307 | String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); 308 | File baseDir = new File(checkout(scmUrl), "dummy-svn"); 309 | 310 | when(fs.baseDir()).thenReturn(baseDir); 311 | String relativePath = "src/main/java/org/dummy2/dummy/Dummy.java"; 312 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath) 313 | .setLines(28) 314 | .setModuleBaseDir(baseDir.toPath()) 315 | .build(); 316 | 317 | Path filepath = new File(baseDir, relativePath).toPath(); 318 | Files.createDirectories(filepath.getParent()); 319 | Files.write(filepath, "package org.dummy;\npublic class Dummy {}".getBytes()); 320 | 321 | BlameOutput blameResult = mock(BlameOutput.class); 322 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 323 | 324 | newSvnBlameCommand().blame(input, blameResult); 325 | verifyZeroInteractions(blameResult); 326 | } 327 | 328 | private static void javaUnzip(File zip, File toDir) { 329 | try { 330 | ZipFile zipFile = new ZipFile(zip); 331 | try { 332 | Enumeration entries = zipFile.entries(); 333 | while (entries.hasMoreElements()) { 334 | ZipEntry entry = entries.nextElement(); 335 | File to = new File(toDir, entry.getName()); 336 | if (entry.isDirectory()) { 337 | Files.createDirectories(to.toPath()); 338 | } else { 339 | File parent = to.getParentFile(); 340 | if (parent != null) { 341 | Files.createDirectories(parent.toPath()); 342 | } 343 | 344 | Files.copy(zipFile.getInputStream(entry), to.toPath()); 345 | } 346 | } 347 | } finally { 348 | zipFile.close(); 349 | } 350 | } catch (Exception e) { 351 | throw new IllegalStateException("Fail to unzip " + zip + " to " + toDir, e); 352 | } 353 | } 354 | 355 | private static String unixPath(File file) { 356 | return file.getAbsolutePath().replace('\\', '/'); 357 | } 358 | 359 | private SvnBlameCommand newSvnBlameCommand() { 360 | return new SvnBlameCommand(mock(SvnConfiguration.class)); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import org.junit.Rule; 24 | import org.junit.Test; 25 | import org.junit.rules.TemporaryFolder; 26 | import org.sonar.api.config.PropertyDefinitions; 27 | import org.sonar.api.config.internal.MapSettings; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.assertj.core.api.Assertions.fail; 31 | 32 | public class SvnConfigurationTest { 33 | 34 | @Rule 35 | public TemporaryFolder temp = new TemporaryFolder(); 36 | 37 | @Test 38 | public void sanityCheck() throws Exception { 39 | MapSettings settings = new MapSettings(new PropertyDefinitions(SvnConfiguration.getProperties())); 40 | SvnConfiguration config = new SvnConfiguration(settings.asConfig()); 41 | 42 | assertThat(config.username()).isNull(); 43 | assertThat(config.password()).isNull(); 44 | 45 | settings.setProperty(SvnConfiguration.USER_PROP_KEY, "foo"); 46 | assertThat(config.username()).isEqualTo("foo"); 47 | 48 | settings.setProperty(SvnConfiguration.PASSWORD_PROP_KEY, "pwd"); 49 | assertThat(config.password()).isEqualTo("pwd"); 50 | 51 | settings.setProperty(SvnConfiguration.PASSPHRASE_PROP_KEY, "pass"); 52 | assertThat(config.passPhrase()).isEqualTo("pass"); 53 | 54 | assertThat(config.privateKey()).isNull(); 55 | File fakeKey = temp.newFile(); 56 | settings.setProperty(SvnConfiguration.PRIVATE_KEY_PATH_PROP_KEY, fakeKey.getAbsolutePath()); 57 | assertThat(config.privateKey()).isEqualTo(fakeKey); 58 | 59 | settings.setProperty(SvnConfiguration.PRIVATE_KEY_PATH_PROP_KEY, "/not/exists"); 60 | try { 61 | config.privateKey(); 62 | fail("Expected exception"); 63 | } catch (Exception e) { 64 | assertThat(e).hasMessageContaining("Unable to read private key from "); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import org.junit.Test; 23 | import org.sonar.api.Plugin; 24 | import org.sonar.api.SonarEdition; 25 | import org.sonar.api.SonarQubeSide; 26 | import org.sonar.api.SonarRuntime; 27 | import org.sonar.api.internal.SonarRuntimeImpl; 28 | import org.sonar.api.utils.Version; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.when; 33 | import static org.sonar.plugins.scm.svn.SvnPlugin.newSvnClientManager; 34 | 35 | public class SvnPluginTest { 36 | @Test 37 | public void getExtensions() { 38 | SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 9), SonarQubeSide.SCANNER, SonarEdition.DEVELOPER); 39 | Plugin.Context context = new Plugin.Context(runtime); 40 | new SvnPlugin().define(context); 41 | assertThat(context.getExtensions()).isNotEmpty(); 42 | } 43 | 44 | @Test 45 | public void newSvnClientManager_with_auth() { 46 | SvnConfiguration config = mock(SvnConfiguration.class); 47 | when(config.password()).thenReturn("password"); 48 | when(config.passPhrase()).thenReturn("passPhrase"); 49 | assertThat(newSvnClientManager(config)).isNotNull(); 50 | } 51 | 52 | @Test 53 | public void newSvnClientManager_without_auth() { 54 | SvnConfiguration config = mock(SvnConfiguration.class); 55 | assertThat(config.password()).isNull(); 56 | assertThat(config.passPhrase()).isNull(); 57 | assertThat(newSvnClientManager(config)).isNotNull(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.time.Instant; 28 | import java.util.Collections; 29 | import java.util.Set; 30 | import org.junit.Before; 31 | import org.junit.Rule; 32 | import org.junit.Test; 33 | import org.junit.rules.ExpectedException; 34 | import org.junit.rules.TemporaryFolder; 35 | import org.sonar.api.batch.scm.ScmProvider; 36 | import org.sonar.api.internal.google.common.collect.ImmutableMap; 37 | import org.sonar.api.internal.google.common.collect.ImmutableSet; 38 | import org.tmatesoft.svn.core.SVNCancelException; 39 | import org.tmatesoft.svn.core.SVNException; 40 | import org.tmatesoft.svn.core.SVNURL; 41 | import org.tmatesoft.svn.core.wc.SVNClientManager; 42 | import org.tmatesoft.svn.core.wc.SVNInfo; 43 | import org.tmatesoft.svn.core.wc.SVNLogClient; 44 | import org.tmatesoft.svn.core.wc.SVNWCClient; 45 | 46 | import static org.assertj.core.api.Assertions.assertThat; 47 | import static org.mockito.Mockito.any; 48 | import static org.mockito.Mockito.mock; 49 | import static org.mockito.Mockito.when; 50 | 51 | public class SvnScmProviderTest { 52 | 53 | // Sample content for unified diffs 54 | // http://www.gnu.org/software/diffutils/manual/html_node/Example-Unified.html#Example-Unified 55 | private static final String CONTENT_LAO = "The Way that can be told of is not the eternal Way;\n" 56 | + "The name that can be named is not the eternal name.\n" 57 | + "The Nameless is the origin of Heaven and Earth;\n" 58 | + "The Named is the mother of all things.\n" 59 | + "Therefore let there always be non-being,\n" 60 | + " so we may see their subtlety,\n" 61 | + "And let there always be being,\n" 62 | + " so we may see their outcome.\n" 63 | + "The two are the same,\n" 64 | + "But after they are produced,\n" 65 | + " they have different names.\n"; 66 | 67 | private static final String CONTENT_TZU = "The Nameless is the origin of Heaven and Earth;\n" 68 | + "The named is the mother of all things.\n" 69 | + "\n" 70 | + "Therefore let there always be non-being,\n" 71 | + " so we may see their subtlety,\n" 72 | + "And let there always be being,\n" 73 | + " so we may see their outcome.\n" 74 | + "The two are the same,\n" 75 | + "But after they are produced,\n" 76 | + " they have different names.\n" 77 | + "They both may be called deep and profound.\n" 78 | + "Deeper and more profound,\n" 79 | + "The door of all subtleties!"; 80 | 81 | @Rule 82 | public TemporaryFolder temp = new TemporaryFolder(); 83 | 84 | @Rule 85 | public ExpectedException thrown = ExpectedException.none(); 86 | 87 | private FindFork findFork = mock(FindFork.class); 88 | private SvnConfiguration config = mock(SvnConfiguration.class); 89 | private SvnTester svnTester; 90 | 91 | @Before 92 | public void before() throws IOException, SVNException { 93 | svnTester = new SvnTester(temp.newFolder().toPath()); 94 | 95 | Path worktree = temp.newFolder().toPath(); 96 | svnTester.checkout(worktree, "trunk"); 97 | createAndCommitFile(worktree, "file-in-first-commit.xoo"); 98 | } 99 | 100 | @Test 101 | public void sanityCheck() { 102 | SvnBlameCommand blameCommand = new SvnBlameCommand(config); 103 | SvnScmProvider svnScmProvider = new SvnScmProvider(config, blameCommand, findFork); 104 | assertThat(svnScmProvider.key()).isEqualTo("svn"); 105 | assertThat(svnScmProvider.blameCommand()).isEqualTo(blameCommand); 106 | } 107 | 108 | @Test 109 | public void testAutodetection() throws IOException { 110 | ScmProvider scmBranchProvider = newScmProvider(); 111 | 112 | File baseDirEmpty = temp.newFolder(); 113 | assertThat(scmBranchProvider.supports(baseDirEmpty)).isFalse(); 114 | 115 | File svnBaseDir = temp.newFolder(); 116 | Files.createDirectory(svnBaseDir.toPath().resolve(".svn")); 117 | assertThat(scmBranchProvider.supports(svnBaseDir)).isTrue(); 118 | 119 | File svnBaseDirSubFolder = temp.newFolder(); 120 | Files.createDirectory(svnBaseDirSubFolder.toPath().resolve(".svn")); 121 | File projectBaseDir = new File(svnBaseDirSubFolder, "folder"); 122 | Files.createDirectory(projectBaseDir.toPath()); 123 | assertThat(scmBranchProvider.supports(projectBaseDir)).isTrue(); 124 | } 125 | 126 | @Test 127 | public void branchChangedFiles_and_lines_from_diverged() throws IOException, SVNException { 128 | Path trunk = temp.newFolder().toPath(); 129 | svnTester.checkout(trunk, "trunk"); 130 | createAndCommitFile(trunk, "file-m1.xoo"); 131 | createAndCommitFile(trunk, "file-m2.xoo"); 132 | createAndCommitFile(trunk, "file-m3.xoo"); 133 | createAndCommitFile(trunk, "lao.txt", CONTENT_LAO); 134 | 135 | // create branch from trunk 136 | svnTester.createBranch("b1"); 137 | 138 | // still on trunk 139 | appendToAndCommitFile(trunk, "file-m3.xoo"); 140 | createAndCommitFile(trunk, "file-m4.xoo"); 141 | 142 | Path b1 = temp.newFolder().toPath(); 143 | svnTester.checkout(b1, "branches/b1"); 144 | Files.createDirectories(b1.resolve("sub")); 145 | createAndCommitFile(b1, "sub/file-b1.xoo"); 146 | appendToAndCommitFile(b1, "file-m1.xoo"); 147 | deleteAndCommitFile(b1, "file-m2.xoo"); 148 | 149 | createAndCommitFile(b1, "file-m5.xoo"); 150 | deleteAndCommitFile(b1, "file-m5.xoo"); 151 | 152 | svnCopyAndCommitFile(b1, "file-m1.xoo", "file-m1-copy.xoo"); 153 | appendToAndCommitFile(b1, "file-m1.xoo"); 154 | 155 | // modify file without committing it -> should not be included (think generated files) 156 | svnTester.appendToFile(b1, "file-m3.xoo"); 157 | 158 | svnTester.update(b1); 159 | 160 | Set changedFiles = newScmProvider().branchChangedFiles("trunk", b1); 161 | assertThat(changedFiles) 162 | .containsExactlyInAnyOrder( 163 | b1.resolve("sub/file-b1.xoo"), 164 | b1.resolve("file-m1.xoo"), 165 | b1.resolve("file-m1-copy.xoo")); 166 | 167 | // use a subset of changed files for .branchChangedLines to verify only requested files are returned 168 | assertThat(changedFiles.remove(b1.resolve("sub/file-b1.xoo"))).isTrue(); 169 | 170 | // generate common sample diff 171 | createAndCommitFile(b1, "lao.txt", CONTENT_TZU); 172 | changedFiles.add(b1.resolve("lao.txt")); 173 | 174 | // a file that should not yield any results 175 | changedFiles.add(b1.resolve("nonexistent")); 176 | 177 | // modify file without committing to it 178 | svnTester.appendToFile(b1, "file-m1.xoo"); 179 | 180 | assertThat(newScmProvider().branchChangedLines("trunk", b1, changedFiles)) 181 | .isEqualTo( 182 | ImmutableMap.of( 183 | b1.resolve("lao.txt"), ImmutableSet.of(2, 3, 11, 12, 13), 184 | b1.resolve("file-m1.xoo"), ImmutableSet.of(2, 3, 4), 185 | b1.resolve("file-m1-copy.xoo"), ImmutableSet.of(1, 2))); 186 | 187 | assertThat(newScmProvider().branchChangedLines("trunk", b1, Collections.singleton(b1.resolve("nonexistent")))) 188 | .isEmpty(); 189 | } 190 | 191 | @Test 192 | public void branchChangedFiles_should_return_empty_when_no_local_changes() throws IOException, SVNException { 193 | Path b1 = temp.newFolder().toPath(); 194 | svnTester.createBranch("b1"); 195 | svnTester.checkout(b1, "branches/b1"); 196 | 197 | assertThat(newScmProvider().branchChangedFiles("b1", b1)).isEmpty(); 198 | } 199 | 200 | @Test 201 | public void branchChangedFiles_should_return_null_when_repo_nonexistent() throws IOException { 202 | assertThat(newScmProvider().branchChangedFiles("trunk", temp.newFolder().toPath())).isNull(); 203 | } 204 | 205 | @Test 206 | public void branchChangedFiles_should_return_null_when_dir_nonexistent() { 207 | assertThat(newScmProvider().branchChangedFiles("trunk", temp.getRoot().toPath().resolve("nonexistent"))).isNull(); 208 | } 209 | 210 | @Test 211 | public void branchChangedLines_should_return_null_when_repo_nonexistent() throws IOException { 212 | assertThat(newScmProvider().branchChangedLines("trunk", temp.newFolder().toPath(), Collections.emptySet())).isNull(); 213 | } 214 | 215 | @Test 216 | public void branchChangedLines_should_return_null_when_dir_nonexistent() { 217 | assertThat(newScmProvider().branchChangedLines("trunk", temp.getRoot().toPath().resolve("nonexistent"), Collections.emptySet())).isNull(); 218 | } 219 | 220 | @Test 221 | public void branchChangedLines_should_return_empty_when_no_local_changes() throws IOException, SVNException { 222 | Path b1 = temp.newFolder().toPath(); 223 | svnTester.createBranch("b1"); 224 | svnTester.checkout(b1, "branches/b1"); 225 | 226 | assertThat(newScmProvider().branchChangedLines("b1", b1, Collections.emptySet())).isEmpty(); 227 | } 228 | 229 | @Test 230 | public void branchChangedLines_should_return_null_when_invalid_diff_format() throws IOException, SVNException { 231 | Path b1 = temp.newFolder().toPath(); 232 | svnTester.createBranch("b1"); 233 | svnTester.checkout(b1, "branches/b1"); 234 | 235 | SvnScmProvider scmProvider = new SvnScmProvider(config, new SvnBlameCommand(config), findFork) { 236 | @Override 237 | ChangedLinesComputer newChangedLinesComputer(Path rootBaseDir, Set changedFiles) { 238 | throw new IllegalStateException("crash"); 239 | } 240 | }; 241 | assertThat(scmProvider.branchChangedLines("b1", b1, Collections.emptySet())).isNull(); 242 | } 243 | 244 | @Test 245 | public void forkDate_returns_null_if_no_fork_found() { 246 | assertThat(new SvnScmProvider(config, new SvnBlameCommand(config), findFork).forkDate(Paths.get(""), "branch")).isNull(); 247 | } 248 | 249 | @Test 250 | public void forkDate_returns_instant_if_fork_found() throws SVNException { 251 | Path rootBaseDir = Paths.get(""); 252 | String referenceBranch = "branch"; 253 | Instant forkDate = Instant.ofEpochMilli(123456789L); 254 | SvnScmProvider provider = new SvnScmProvider(config, new SvnBlameCommand(config), findFork); 255 | when(findFork.findDate(rootBaseDir, referenceBranch)).thenReturn(forkDate); 256 | 257 | assertThat(provider.forkDate(rootBaseDir, referenceBranch)).isEqualTo(forkDate); 258 | } 259 | 260 | @Test 261 | public void forkDate_returns_null_if_exception_occurs() throws SVNException { 262 | Path rootBaseDir = Paths.get(""); 263 | String referenceBranch = "branch"; 264 | SvnScmProvider provider = new SvnScmProvider(config, new SvnBlameCommand(config), findFork); 265 | when(findFork.findDate(rootBaseDir, referenceBranch)).thenThrow(new SVNCancelException()); 266 | 267 | assertThat(provider.forkDate(rootBaseDir, referenceBranch)).isNull(); 268 | } 269 | 270 | @Test 271 | public void computeChangedPaths_should_not_crash_when_getRepositoryRootURL_getPath_is_empty() throws SVNException { 272 | // verify assumptions about what SVNKit returns as svn root path for urls like http://svnserver/ 273 | assertThat(SVNURL.parseURIEncoded("http://svnserver/").getPath()).isEmpty(); 274 | assertThat(SVNURL.parseURIEncoded("http://svnserver").getPath()).isEmpty(); 275 | 276 | SVNClientManager svnClientManagerMock = mock(SVNClientManager.class); 277 | 278 | SVNWCClient svnwcClientMock = mock(SVNWCClient.class); 279 | when(svnClientManagerMock.getWCClient()).thenReturn(svnwcClientMock); 280 | 281 | SVNLogClient svnLogClient = mock(SVNLogClient.class); 282 | when(svnClientManagerMock.getLogClient()).thenReturn(svnLogClient); 283 | 284 | SVNInfo svnInfoMock = mock(SVNInfo.class); 285 | when(svnwcClientMock.doInfo(any(), any())).thenReturn(svnInfoMock); 286 | 287 | // Simulate repository root on /, SVNKIT then returns an repository root url WITHOUT / at the end. 288 | when(svnInfoMock.getRepositoryRootURL()).thenReturn(SVNURL.parseURIEncoded("http://svnserver")); 289 | when(svnInfoMock.getURL()).thenReturn(SVNURL.parseURIEncoded("http://svnserver/myproject/trunk/")); 290 | 291 | assertThat(SvnScmProvider.computeChangedPaths(Paths.get("/"), svnClientManagerMock)).isEmpty(); 292 | } 293 | 294 | private void createAndCommitFile(Path worktree, String filename, String content) throws IOException, SVNException { 295 | svnTester.createFile(worktree, filename, content); 296 | svnTester.add(worktree, filename); 297 | svnTester.commit(worktree); 298 | } 299 | 300 | private void createAndCommitFile(Path worktree, String filename) throws IOException, SVNException { 301 | createAndCommitFile(worktree, filename, filename + "\n"); 302 | } 303 | 304 | private void appendToAndCommitFile(Path worktree, String filename) throws IOException, SVNException { 305 | svnTester.appendToFile(worktree, filename); 306 | svnTester.commit(worktree); 307 | } 308 | 309 | private void deleteAndCommitFile(Path worktree, String filename) throws IOException, SVNException { 310 | svnTester.deleteFile(worktree, filename); 311 | svnTester.commit(worktree); 312 | } 313 | 314 | private void svnCopyAndCommitFile(Path worktree, String src, String dst) throws SVNException { 315 | svnTester.copy(worktree, src, dst); 316 | svnTester.commit(worktree); 317 | } 318 | 319 | private SvnScmProvider newScmProvider() { 320 | return new SvnScmProvider(config, new SvnBlameCommand(config), findFork); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnTester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.StandardOpenOption; 27 | import java.util.Collection; 28 | import java.util.HashSet; 29 | import java.util.Set; 30 | import org.tmatesoft.svn.core.SVNDepth; 31 | import org.tmatesoft.svn.core.SVNException; 32 | import org.tmatesoft.svn.core.SVNURL; 33 | import org.tmatesoft.svn.core.io.SVNRepositoryFactory; 34 | import org.tmatesoft.svn.core.wc.SVNClientManager; 35 | import org.tmatesoft.svn.core.wc.SVNCopyClient; 36 | import org.tmatesoft.svn.core.wc.SVNCopySource; 37 | import org.tmatesoft.svn.core.wc.SVNRevision; 38 | import org.tmatesoft.svn.core.wc.SVNUpdateClient; 39 | import org.tmatesoft.svn.core.wc2.SvnList; 40 | import org.tmatesoft.svn.core.wc2.SvnOperationFactory; 41 | import org.tmatesoft.svn.core.wc2.SvnRemoteMkDir; 42 | import org.tmatesoft.svn.core.wc2.SvnTarget; 43 | 44 | public class SvnTester { 45 | private final SVNClientManager manager = SVNClientManager.newInstance(new SvnOperationFactory()); 46 | 47 | private final SVNURL localRepository; 48 | 49 | public SvnTester(Path root) throws SVNException, IOException { 50 | localRepository = SVNRepositoryFactory.createLocalRepository(root.toFile(), false, false); 51 | mkdir("trunk"); 52 | mkdir("branches"); 53 | } 54 | 55 | private void mkdir(String relpath) throws IOException, SVNException { 56 | SvnRemoteMkDir remoteMkDir = manager.getOperationFactory().createRemoteMkDir(); 57 | remoteMkDir.addTarget(SvnTarget.fromURL(localRepository.appendPath(relpath, false))); 58 | remoteMkDir.run(); 59 | } 60 | 61 | public void createBranch(String branchName) throws IOException, SVNException { 62 | SVNCopyClient copyClient = manager.getCopyClient(); 63 | SVNCopySource source = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, localRepository.appendPath("trunk", false)); 64 | copyClient.doCopy(new SVNCopySource[] {source}, localRepository.appendPath("branches/" + branchName, false), false, false, true, "Create branch", null); 65 | } 66 | 67 | public void createBranch(String branchSource, String branchName) throws IOException, SVNException { 68 | SVNCopyClient copyClient = manager.getCopyClient(); 69 | SVNCopySource source = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, localRepository.appendPath(branchSource, false)); 70 | copyClient.doCopy(new SVNCopySource[] {source}, localRepository.appendPath("branches/" + branchName, false), false, false, true, "Create branch", null); 71 | } 72 | 73 | public void checkout(Path worktree, String path) throws SVNException { 74 | SVNUpdateClient updateClient = manager.getUpdateClient(); 75 | updateClient.doCheckout(localRepository.appendPath(path, false), 76 | worktree.toFile(), null, null, SVNDepth.INFINITY, false); 77 | } 78 | 79 | public void add(Path worktree, String filename) throws SVNException { 80 | manager.getWCClient().doAdd(worktree.resolve(filename).toFile(), true, false, false, SVNDepth.INFINITY, false, false, true); 81 | } 82 | 83 | public void copy(Path worktree, String src, String dst) throws SVNException { 84 | SVNCopyClient copyClient = manager.getCopyClient(); 85 | SVNCopySource source = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, worktree.resolve(src).toFile()); 86 | copyClient.doCopy(new SVNCopySource[]{source}, worktree.resolve(dst).toFile(), false, false, true); 87 | } 88 | 89 | public void commit(Path worktree) throws SVNException { 90 | manager.getCommitClient().doCommit(new File[] {worktree.toFile()}, false, "commit " + worktree, null, null, false, false, SVNDepth.INFINITY); 91 | } 92 | 93 | public void update(Path worktree) throws SVNException { 94 | manager.getUpdateClient().doUpdate(new File[] {worktree.toFile()}, SVNRevision.HEAD, SVNDepth.INFINITY, false, false); 95 | } 96 | 97 | public Collection list(String... paths) throws SVNException { 98 | Set results = new HashSet<>(); 99 | 100 | SvnList list = manager.getOperationFactory().createList(); 101 | if (paths.length == 0) { 102 | list.addTarget(SvnTarget.fromURL(localRepository)); 103 | } else { 104 | for (String path : paths) { 105 | list.addTarget(SvnTarget.fromURL(localRepository.appendPath(path, false))); 106 | } 107 | } 108 | list.setDepth(SVNDepth.INFINITY); 109 | list.setReceiver((svnTarget, svnDirEntry) -> { 110 | String path = svnDirEntry.getRelativePath(); 111 | if (!path.isEmpty()) { 112 | results.add(path); 113 | } 114 | }); 115 | list.run(); 116 | 117 | return results; 118 | } 119 | 120 | public void createFile(Path worktree, String filename, String content) throws IOException { 121 | Files.write(worktree.resolve(filename), content.getBytes()); 122 | } 123 | 124 | public void createFile(Path worktree, String filename) throws IOException { 125 | createFile(worktree, filename, filename + "\n"); 126 | } 127 | 128 | public void appendToFile(Path worktree, String filename) throws IOException { 129 | Files.write(worktree.resolve(filename), (filename + "\n").getBytes(), StandardOpenOption.APPEND); 130 | } 131 | 132 | public void deleteFile(Path worktree, String filename) throws SVNException { 133 | manager.getWCClient().doDelete(worktree.resolve(filename).toFile(), false, false); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnTesterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: SVN 3 | * Copyright (C) 2014-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.plugins.scm.svn; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import org.junit.Before; 25 | import org.junit.Rule; 26 | import org.junit.Test; 27 | import org.junit.rules.TemporaryFolder; 28 | import org.tmatesoft.svn.core.SVNException; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | 32 | public class SvnTesterTest { 33 | @Rule 34 | public TemporaryFolder temp = new TemporaryFolder(); 35 | 36 | private SvnTester tester; 37 | 38 | @Before 39 | public void before() throws IOException, SVNException { 40 | tester = new SvnTester(temp.newFolder().toPath()); 41 | } 42 | 43 | @Test 44 | public void test_init() throws SVNException { 45 | assertThat(tester.list()).containsExactlyInAnyOrder("trunk", "branches"); 46 | } 47 | 48 | @Test 49 | public void test_add_and_commit() throws IOException, SVNException { 50 | assertThat(tester.list("trunk")).isEmpty(); 51 | 52 | Path worktree = temp.newFolder().toPath(); 53 | tester.checkout(worktree, "trunk"); 54 | tester.createFile(worktree, "file1"); 55 | 56 | tester.add(worktree, "file1"); 57 | tester.commit(worktree); 58 | 59 | assertThat(tester.list("trunk")).containsOnly("file1"); 60 | } 61 | 62 | @Test 63 | public void test_createBranch() throws IOException, SVNException { 64 | tester.createBranch("b1"); 65 | assertThat(tester.list()).containsExactlyInAnyOrder("trunk", "branches", "branches/b1"); 66 | assertThat(tester.list("branches")).containsOnly("b1"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/resources/blame-with-anonymous-commit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | simon.brandhof 10 | 2009-04-18T10:29:59.077093Z 11 | 12 | 13 | 15 | 17 | 2009-04-01T10:29:59.077093Z 18 | 19 | 20 | 22 | 24 | david 25 | 2009-08-31T22:32:17.361675Z 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/resources/blame-with-merge-history.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | automatic-merge 10 | 2009-04-18T10:29:59.077093Z 11 | 12 | 14 | 16 | dgageot 17 | 2012-07-19T09:44:57.393222Z 18 | 19 | 20 | 21 | 23 | 25 | simon.brandhof 26 | 2009-04-18T10:29:59.077093Z 27 | 28 | 30 | 32 | simon.brandhof 33 | 2009-04-18T10:29:59.077093Z 34 | 35 | 36 | 37 | 39 | 41 | david 42 | 2009-08-31T22:32:17.361675Z 43 | 44 | 46 | 48 | david 49 | 2009-08-31T22:32:17.361675Z 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/resources/blame-with-uncomitted-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | simon.brandhof 10 | 2009-04-18T10:29:59.077093Z 11 | 12 | 13 | 15 | 16 | 18 | 20 | david 21 | 2009-08-31T22:32:17.361675Z 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/src/test/resources/blame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | simon.brandhof 10 | 2009-04-18T10:29:59.077093Z 11 | 12 | 13 | 15 | 17 | simon.brandhof 18 | 2009-04-18T10:29:59.077093Z 19 | 20 | 21 | 23 | 25 | david 26 | 2009-08-31T22:32:17.361675Z 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.6/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.6/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.6/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.6/repo-svn.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.7/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.7/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.7/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.7/repo-svn.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.8/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.8/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.8/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.8/repo-svn.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.9/repo-svn-with-merge.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.9/repo-svn-with-merge.zip -------------------------------------------------------------------------------- /sonar-scm-svn-plugin/test-repos/1.9/repo-svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-svn/0767c846b3d4bf306a8a8c3b194b465ebc549498/sonar-scm-svn-plugin/test-repos/1.9/repo-svn.zip -------------------------------------------------------------------------------- /third-party-licenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mvn org.codehaus.mojo:license-maven-plugin:aggregate-add-third-party -Dlicense.includedScopes=compile 3 | 4 | cat target/generated-sources/license/THIRD-PARTY.txt 5 | --------------------------------------------------------------------------------