├── .cirrus.yml ├── .github └── workflows │ ├── dogfood.yml │ └── release.yml ├── .gitignore ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── build.sh ├── its ├── pom.xml ├── scm-repo │ └── dummy-git.zip └── src │ └── test │ └── java │ └── org │ └── sonarsource │ └── scm │ └── git │ └── its │ └── GitTest.java ├── pom.xml ├── sonar-scm-git-plugin ├── pom.xml ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── sonarsource │ │ │ └── scm │ │ │ └── git │ │ │ ├── ChangedLinesComputer.java │ │ │ ├── GitIgnoreCommand.java │ │ │ ├── GitPlugin.java │ │ │ ├── GitScmProvider.java │ │ │ ├── GitThreadFactory.java │ │ │ ├── IncludedFilesRepository.java │ │ │ ├── JGitBlameCommand.java │ │ │ ├── JGitUtils.java │ │ │ └── package-info.java │ └── test │ │ └── java │ │ └── org │ │ └── sonarsource │ │ └── scm │ │ └── git │ │ ├── ChangedLinesComputerTest.java │ │ ├── GitIgnoreCommandTest.java │ │ ├── GitPluginTest.java │ │ ├── GitScmProviderTest.java │ │ ├── GitThreadFactoryTest.java │ │ ├── JGitBlameCommandTest.java │ │ └── Utils.java └── test-repos │ ├── dummy-git-nested.zip │ ├── dummy-git-reference-clone.zip │ ├── dummy-git.zip │ ├── ignore-git.zip │ ├── reference-git.zip │ └── shallow-git.zip └── third-party-licenses.sh /.cirrus.yml: -------------------------------------------------------------------------------- 1 | gcp_credentials: ENCRYPTED[!e5f7207bd8d02d383733bef47e18296ac32e3b7d22eb480354e8dd8fdc0004be45a8a4e72c797bd66ee94eb3340fa363!] 2 | 3 | # 4 | # ENV VARIABLES 5 | # 6 | env: 7 | ### Shared variables 8 | ARTIFACTORY_URL: ENCRYPTED[!2f8fa307d3289faa0aa6791f18b961627ae44f1ef46b136e1a1e63b0b4c86454dbb25520d49b339e2d50a1e1e5f95c88!] 9 | ARTIFACTORY_PRIVATE_USERNAME: repox-private-reader 10 | ARTIFACTORY_PRIVATE_PASSWORD: ENCRYPTED[!35ca4446564213d4fd2d1a96e42a871d5de6e6aac4e1dd3e89342892a60a2badf74a966bcc8e885e9c9d09a775ffe4c0!] 11 | ARTIFACTORY_API_KEY: ENCRYPTED[!35ca4446564213d4fd2d1a96e42a871d5de6e6aac4e1dd3e89342892a60a2badf74a966bcc8e885e9c9d09a775ffe4c0!] 12 | ARTIFACTORY_DEPLOY_USERNAME: repox-qa-deployer 13 | ARTIFACTORY_DEPLOY_PASSWORD: ENCRYPTED[!d484e19f33c9ce63b165f70e414a33b1ac6c215a126791aacbf8059626caf0fd8a78e999a20af5c1a4ba01c0b0247921!] 14 | ARTIFACTORY_DEPLOY_REPO: sonarsource-public-qa 15 | 16 | GCF_ACCESS_TOKEN: ENCRYPTED[!1fb91961a5c01e06e38834e55755231d649dc62eca354593105af9f9d643d701ae4539ab6a8021278b8d9348ae2ce8be!] 17 | PROMOTE_URL: ENCRYPTED[!e22ed2e34a8f7a1aea5cff653585429bbd3d5151e7201022140218f9c5d620069ec2388f14f83971e3fd726215bc0f5e!] 18 | 19 | GITHUB_TOKEN: ENCRYPTED[!f272985ea5b49b3cf9c414b98de6a8e9096be47bfcee52f33311ba3131a2af637c1b956f49585b7757dd84b7c030233a!] 20 | 21 | BURGR_URL: ENCRYPTED[!c7e294da94762d7bac144abef6310c5db300c95979daed4454ca977776bfd5edeb557e1237e3aa8ed722336243af2d78!] 22 | BURGR_USERNAME: ENCRYPTED[!b29ddc7610116de511e74bec9a93ad9b8a20ac217a0852e94a96d0066e6e822b95e7bc1fe152afb707f16b70605fddd3!] 23 | BURGR_PASSWORD: ENCRYPTED[!83e130718e92b8c9de7c5226355f730e55fb46e45869149a9223e724bb99656878ef9684c5f8cfef434aa716e87f4cf2!] 24 | 25 | ### Project variables 26 | DEPLOY_PULL_REQUEST: true 27 | ARTIFACTS: org.sonarsource.scm.git:sonar-scm-git:jar 28 | 29 | # 30 | # RE-USABLE CONFIGS 31 | # 32 | container_definition: &CONTAINER_DEFINITION 33 | image: us.gcr.io/sonarqube-team/base:j11-m3-latest 34 | cluster_name: cirrus-ci-cluster 35 | zone: us-central1-a 36 | namespace: default 37 | 38 | only_sonarsource_qa: &ONLY_SONARSOURCE_QA 39 | only_if: $CIRRUS_USER_COLLABORATOR == 'true' && ($CIRRUS_PR != "" || $CIRRUS_BRANCH == "master" || $CIRRUS_BRANCH =~ "branch-.*" || $CIRRUS_BRANCH =~ "dogfood-on-.*") 40 | 41 | # 42 | # TASKS 43 | # 44 | build_task: 45 | gke_container: 46 | <<: *CONTAINER_DEFINITION 47 | cpu: 2 48 | memory: 2G 49 | env: 50 | SONAR_TOKEN: ENCRYPTED[!b6fd814826c51e64ee61b0b6f3ae621551f6413383f7170f73580e2e141ac78c4b134b506f6288c74faa0dd564c05a29!] 51 | SONAR_HOST_URL: https://next.sonarqube.com/sonarqube 52 | SIGN_KEY: ENCRYPTED[!cc216dfe592f79db8006f2a591f8f98b40aa2b078e92025623594976fd32f6864c1e6b6ba74b50647f608e2418e6c336!] 53 | PGP_PASSPHRASE: ENCRYPTED[!314a8fc344f45e462dd5e8dccd741d7562283a825e78ebca27d4ae9db8e65ce618e7f6aece386b2782a5abe5171467bd!] 54 | maven_cache: 55 | folder: ${CIRRUS_WORKING_DIR}/.m2/repository 56 | script: 57 | - source cirrus-env BUILD 58 | - regular_mvn_build_deploy_analyze 59 | cleanup_before_cache_script: 60 | - cleanup_maven_repository 61 | 62 | qa_task: 63 | depends_on: 64 | - build 65 | <<: *ONLY_SONARSOURCE_QA 66 | gke_container: 67 | <<: *CONTAINER_DEFINITION 68 | cpu: 1.7 69 | memory: 5Gb 70 | env: 71 | matrix: 72 | - SQ_VERSION: LATEST_RELEASE[7.9] 73 | - SQ_VERSION: LATEST_RELEASE[8.4] 74 | maven_cache: 75 | folder: ${CIRRUS_WORKING_DIR}/.m2/repository 76 | qa_script: 77 | - source cirrus-env QA 78 | - source set_maven_build_version $BUILD_NUMBER 79 | - cd its 80 | - mvn verify -Dsonar.runtimeVersion=$SQ_VERSION -e -B -V -U 81 | cleanup_before_cache_script: 82 | - cleanup_maven_repository 83 | 84 | promote_task: 85 | depends_on: 86 | - qa 87 | <<: *ONLY_SONARSOURCE_QA 88 | gke_container: 89 | <<: *CONTAINER_DEFINITION 90 | cpu: 0.5 91 | memory: 500M 92 | maven_cache: 93 | folder: $CIRRUS_WORKING_DIR/.m2/repository 94 | script: 95 | - cirrus_promote_maven 96 | cleanup_before_cache_script: 97 | - cleanup_maven_repository 98 | -------------------------------------------------------------------------------- /.github/workflows/dogfood.yml: -------------------------------------------------------------------------------- 1 | name: Dogfood merge 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - "dogfood/**" 7 | jobs: 8 | dogfood_merge: 9 | runs-on: ubuntu-latest 10 | name: Update dogfood-on-next branch 11 | steps: 12 | - name: Merge dogfood and master branches 13 | uses: SonarSource/gh-action_dogfood_merge@v1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_ORG_TOKEN }} 16 | with: 17 | dogfood-branch: "dogfood-on-next" 18 | - name: Notify failures on Slack 19 | if: failure() 20 | uses: Ilshidur/action-slack@2.0.0 21 | env: 22 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 23 | with: 24 | args: "Failed to merge dogfood and master branches, see the logs at https://github.com/SonarSource/sonar-scm-git/actions" 25 | -------------------------------------------------------------------------------- /.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_release/main@v3 18 | - name: Run release action 19 | id: run_release 20 | with: 21 | distribute: true 22 | uses: SonarSource/gh-action_release/main@v3 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_ORG_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 | maven-central-sync: 44 | runs-on: ubuntu-latest 45 | needs: 46 | - run_release 47 | steps: 48 | - name: Setup JFrog CLI 49 | uses: jfrog/setup-jfrog-cli@v1 50 | - name: JFrog config 51 | run: jfrog rt config repox --url https://repox.jfrog.io/artifactory/ --apikey $ARTIFACTORY_API_KEY --basic-auth-only 52 | env: 53 | ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} 54 | - name: Get the version 55 | id: get_version 56 | run: | 57 | IFS=. read major minor patch build <<< "${{ github.event.release.tag_name }}" 58 | echo ::set-output name=build::"${build}" 59 | - name: Create local repository directory 60 | id: local_repo 61 | run: echo ::set-output name=dir::"$(mktemp -d repo.XXXXXXXX)" 62 | - name: Download Artifacts 63 | uses: SonarSource/gh-action_release/download-build@v3 64 | with: 65 | build-number: ${{ steps.get_version.outputs.build }} 66 | local-repo-dir: ${{ steps.local_repo.outputs.dir }} 67 | - name: Maven Central Sync 68 | id: maven-central-sync 69 | continue-on-error: true 70 | uses: SonarSource/gh-action_release/maven-central-sync@v3 71 | with: 72 | local-repo-dir: ${{ steps.local_repo.outputs.dir }} 73 | env: 74 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 75 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 76 | - name: Notify on failure 77 | if: ${{ failure() || steps.maven-central-sync.outcome == 'failure' }} 78 | uses: 8398a7/action-slack@v3 79 | with: 80 | status: failure 81 | fields: repo,author,eventName 82 | env: 83 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_BUILD_WEBHOOK }} 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---- Maven 2 | target/ 3 | dependency-reduced-pom.xml 4 | 5 | # ---- IntelliJ IDEA 6 | *.iws 7 | *.iml 8 | *.ipr 9 | .idea/ 10 | 11 | # ---- Eclipse 12 | .classpath 13 | .project 14 | .settings 15 | .externalToolBuilders 16 | 17 | # ---- Mac OS X 18 | .DS_Store 19 | Icon? 20 | # Thumbnails 21 | ._* 22 | # Files that might appear on external disk 23 | .Spotlight-V100 24 | .Trashes 25 | 26 | # ---- Windows 27 | # Windows image file caches 28 | Thumbs.db 29 | # Folder config file 30 | Desktop.ini 31 | -------------------------------------------------------------------------------- /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 SCM Git 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 Git Plugin 2 | 3 | [![Build Status](https://api.cirrus-ci.com/github/SonarSource/sonar-scm-git.svg)](https://cirrus-ci.com/github/SonarSource/sonar-scm-git) [![Quality Gate](https://next.sonarqube.com/sonarqube/api/project_badges/measure?project=org.sonarsource.scm.git%3Asonar-scm-git&metric=alert_status)](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.scm.git%3Asonar-scm-git) 4 | 5 | ### Embedded 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/git). 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-2021 SonarSource. 28 | 29 | Licensed under the [GNU Lesser General Public License, Version 3.0](http://www.gnu.org/licenses/lgpl.txt) 30 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn clean package $* 4 | -------------------------------------------------------------------------------- /its/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.sonarsource.scm.git 8 | sonar-scm-git 9 | 1.13.0-SNAPSHOT 10 | 11 | 12 | it-scmgit 13 | Git Plugin Integration Tests 14 | 15 | 16 | true 17 | true 18 | 19 | 20 | 21 | 22 | org.sonarsource.orchestrator 23 | sonar-orchestrator 24 | 3.22.0.1791 25 | test 26 | 27 | 28 | junit 29 | junit 30 | 4.13.1 31 | test 32 | 33 | 34 | org.assertj 35 | assertj-core 36 | 3.11.1 37 | test 38 | 39 | 40 | 41 | 42 | 43 | qa 44 | 45 | 46 | env.SONARSOURCE_QA 47 | true 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-dependency-plugin 55 | 2.10 56 | 57 | 58 | copy-plugin 59 | generate-test-resources 60 | 61 | copy 62 | 63 | 64 | 65 | 66 | ${project.groupId} 67 | sonar-scm-git-plugin 68 | ${project.version} 69 | sonar-plugin 70 | true 71 | 72 | 73 | ../sonar-scm-git-plugin/target 74 | true 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /its/scm-repo/dummy-git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/its/scm-repo/dummy-git.zip -------------------------------------------------------------------------------- /its/src/test/java/org/sonarsource/scm/git/its/GitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Git Plugin 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 org.sonarsource.scm.git.its; 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.text.ParseException; 32 | import java.text.SimpleDateFormat; 33 | import java.util.Date; 34 | import java.util.HashMap; 35 | import java.util.Map; 36 | import org.apache.commons.io.FileUtils; 37 | import org.apache.commons.lang.builder.EqualsBuilder; 38 | import org.apache.commons.lang.builder.HashCodeBuilder; 39 | import org.apache.commons.lang.builder.ToStringBuilder; 40 | import org.apache.commons.lang.builder.ToStringStyle; 41 | import org.assertj.core.data.MapEntry; 42 | import org.junit.Before; 43 | import org.junit.ClassRule; 44 | import org.junit.Rule; 45 | import org.junit.Test; 46 | import org.junit.rules.ExpectedException; 47 | import org.sonar.wsclient.jsonsimple.JSONArray; 48 | import org.sonar.wsclient.jsonsimple.JSONObject; 49 | import org.sonar.wsclient.jsonsimple.JSONValue; 50 | 51 | import static org.assertj.core.api.Assertions.assertThat; 52 | 53 | public class GitTest { 54 | public static final File PROJECTS_DIR = new File("target/projects"); 55 | public static final File SOURCES_DIR = new File("scm-repo"); 56 | 57 | @ClassRule 58 | public static Orchestrator orchestrator = Orchestrator.builderEnv() 59 | .setSonarVersion(getSonarVersion()) 60 | .addPlugin(FileLocation.byWildcardMavenFilename(new File("../sonar-scm-git-plugin/target"), "sonar-scm-git-plugin-*.jar")) 61 | .addPlugin(MavenLocation.of("org.sonarsource.java", "sonar-java-plugin", "LATEST_RELEASE")) 62 | .build(); 63 | 64 | @Rule 65 | public ExpectedException thrown = ExpectedException.none(); 66 | 67 | @Before 68 | public void deleteData() { 69 | orchestrator.resetData(); 70 | } 71 | 72 | /** 73 | * SONARSCGIT-7 Use Git commit date instead of author date" 74 | */ 75 | @Test 76 | public void sample_git_project_commit_date() throws Exception { 77 | unzip("dummy-git.zip"); 78 | 79 | runSonar("dummy-git"); 80 | 81 | assertThat(getScmData("dummy-git:dummy:src/main/java/org/dummy/Dummy.java")) 82 | .contains( 83 | MapEntry.entry(1, new LineData("6b3aab35a3ea32c1636fee56f996e677653c48ea", "2012-07-17T16:12:48+0200", "david@gageot.net")), 84 | MapEntry.entry(2, new LineData("6b3aab35a3ea32c1636fee56f996e677653c48ea", "2012-07-17T16:12:48+0200", "david@gageot.net")), 85 | MapEntry.entry(3, new LineData("6b3aab35a3ea32c1636fee56f996e677653c48ea", "2012-07-17T16:12:48+0200", "david@gageot.net")), 86 | 87 | MapEntry.entry(26, new LineData("0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7", "2015-05-19T13:31:09+0200", "duarte.meneses@sonarsource.com")), 88 | MapEntry.entry(27, new LineData("0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7", "2015-05-19T13:31:09+0200", "duarte.meneses@sonarsource.com")), 89 | MapEntry.entry(28, new LineData("0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7", "2015-05-19T13:31:09+0200", "duarte.meneses@sonarsource.com"))); 90 | } 91 | 92 | @Test 93 | public void dont_fail_on_uncommited_files() throws Exception { 94 | unzip("dummy-git.zip"); 95 | 96 | // Edit file 97 | FileUtils.write(new File(project("dummy-git"), "src/main/java/org/dummy/Dummy.java"), "\n", StandardCharsets.UTF_8, true); 98 | // New file 99 | FileUtils.write(new File(project("dummy-git"), "src/main/java/org/dummy/Dummy2.java"), "package org.dummy;\npublic class Dummy2 {}", StandardCharsets.UTF_8, false); 100 | 101 | BuildResult result = runSonar("dummy-git"); 102 | assertThat(result.getLogs()).contains("Missing blame information for the following files:"); 103 | assertThat(result.getLogs()).contains("src/main/java/org/dummy/Dummy.java"); 104 | assertThat(result.getLogs()).contains("src/main/java/org/dummy/Dummy2.java"); 105 | 106 | if (orchestrator.getServer().version().isGreaterThanOrEquals(7, 1)) { 107 | assertThat(getScmData("dummy-git:dummy:src/main/java/org/dummy/Dummy.java")).hasSize(31); 108 | assertThat(getScmData("dummy-git:dummy:src/main/java/org/dummy/Dummy2.java")).hasSize(2); 109 | } else { 110 | assertThat(getScmData("dummy-git:dummy:src/main/java/org/dummy/Dummy.java")).isEmpty(); 111 | assertThat(getScmData("dummy-git:dummy:src/main/java/org/dummy/Dummy2.java")).isEmpty(); 112 | } 113 | } 114 | 115 | public static void unzip(String zipName) { 116 | try { 117 | FileUtils.deleteQuietly(PROJECTS_DIR); 118 | FileUtils.forceMkdir(PROJECTS_DIR); 119 | ZipUtils.unzip(new File(SOURCES_DIR, zipName), PROJECTS_DIR); 120 | } catch (IOException e) { 121 | throw new IllegalStateException(e); 122 | } 123 | } 124 | 125 | public static BuildResult runSonar(String projectName, String... keyValues) { 126 | File pom = new File(project(projectName), "pom.xml"); 127 | 128 | MavenBuild install = MavenBuild.create(pom).setGoals("clean install"); 129 | MavenBuild sonar = MavenBuild.create(pom).setGoals("sonar:sonar"); 130 | sonar.setProperty("sonar.scm.disabled", "false"); 131 | sonar.setProperties(keyValues); 132 | orchestrator.executeBuild(install); 133 | return orchestrator.executeBuild(sonar); 134 | } 135 | 136 | public static File project(String name) { 137 | return new File(PROJECTS_DIR, name); 138 | } 139 | 140 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); 141 | private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 142 | 143 | private class LineData { 144 | 145 | final String revision; 146 | final Date date; 147 | final String author; 148 | 149 | public LineData(String revision, String datetime, String author) throws ParseException { 150 | this.revision = revision; 151 | this.date = DATETIME_FORMAT.parse(datetime); 152 | this.author = author; 153 | } 154 | 155 | public LineData(String date, String author) throws ParseException { 156 | this.revision = null; 157 | this.date = DATE_FORMAT.parse(date); 158 | this.author = author; 159 | } 160 | 161 | @Override 162 | public boolean equals(Object obj) { 163 | return EqualsBuilder.reflectionEquals(this, obj); 164 | } 165 | 166 | @Override 167 | public int hashCode() { 168 | return new HashCodeBuilder().append(revision).append(date).append(author).toHashCode(); 169 | } 170 | 171 | @Override 172 | public String toString() { 173 | return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE); 174 | } 175 | } 176 | 177 | private Map getScmData(String fileKey) throws ParseException { 178 | Map result = new HashMap<>(); 179 | String json = orchestrator.getServer().adminWsClient().get("api/sources/scm", "commits_by_line", "true", "key", fileKey); 180 | JSONObject obj = (JSONObject) JSONValue.parse(json); 181 | JSONArray array = (JSONArray) obj.get("scm"); 182 | for (Object anArray : array) { 183 | JSONArray item = (JSONArray) anArray; 184 | String dateOrDatetime = (String) item.get(2); 185 | result.put(((Long) item.get(0)).intValue(), new LineData((String) item.get(3), 186 | dateOrDatetime, (String) item.get(1))); 187 | } 188 | return result; 189 | } 190 | 191 | private static String getSonarVersion() { 192 | String versionProperty = System.getProperty("sonar.runtimeVersion"); 193 | return versionProperty != null ? versionProperty : "LATEST_RELEASE"; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.sonarsource.parent 6 | parent 7 | 59.0.29 8 | 9 | org.sonarsource.scm.git 10 | sonar-scm-git 11 | 1.13.0-SNAPSHOT 12 | pom 13 | SonarQube SCM Git 14 | Git SCM Provider for SonarQube 15 | http://redirect.sonarsource.com/plugins/scmgit.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-git-plugin 41 | 42 | 43 | 44 | scm:git:git@github.com:SonarSource/sonar-scm-git.git 45 | scm:git:git@github.com:SonarSource/sonar-scm-git.git 46 | https://github.com/SonarSource/sonar-scm-git 47 | HEAD 48 | 49 | 50 | 51 | jira 52 | https://jira.sonarsource.com/browse/SONARSCGIT 53 | 54 | 55 | 56 | travis-ci 57 | https://travis-ci.org/SonarSource/sonar-scm-git 58 | 59 | 60 | 61 | 62 | sonar-scm-git 63 | 64 | 65 | 3.2 66 | 67 | 68 | ${project.groupId}:sonar-scm-git-plugin:jar 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-javadoc-plugin 77 | 78 | 8 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | its 88 | 89 | its 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.sonarsource.scm.git 7 | sonar-scm-git 8 | 1.13.0-SNAPSHOT 9 | 10 | sonar-scm-git-plugin 11 | sonar-plugin 12 | SonarQube :: Plugins :: SCM :: Git 13 | Git SCM Provider for SonarQube 14 | http://redirect.sonarsource.com/plugins/scmgit.html 15 | 16 | 17 | https://github.com/SonarSource/sonar-scm-git 18 | 19 | 20 | 21 | 22 | 7.9 23 | Git 24 | org.sonarsource.scm.git.GitPlugin 25 | 26 | 27 | 28 | 29 | com.google.code.findbugs 30 | jsr305 31 | 2.0.3 32 | provided 33 | 34 | 35 | org.sonarsource.sonarqube 36 | sonar-plugin-api 37 | ${sonar.apiVersion} 38 | provided 39 | 40 | 41 | org.eclipse.jgit 42 | org.eclipse.jgit 43 | 5.9.0.202009080501-r 44 | 45 | 46 | commons-logging 47 | commons-logging 48 | 49 | 50 | 51 | 52 | 53 | 54 | junit 55 | junit 56 | 4.13.1 57 | test 58 | 59 | 60 | org.assertj 61 | assertj-core 62 | 3.11.1 63 | test 64 | 65 | 66 | org.mockito 67 | mockito-core 68 | 2.22.0 69 | test 70 | 71 | 72 | commons-io 73 | commons-io 74 | 2.6 75 | test 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.sonarsource.sonar-packaging-maven-plugin 83 | sonar-packaging-maven-plugin 84 | 85 | Git 86 | true 87 | org.sonarsource.scm.git.GitPlugin 88 | 7.9 89 | 90 | 91 | 92 | maven-shade-plugin 93 | 94 | 95 | package 96 | 97 | shade 98 | 99 | 100 | false 101 | true 102 | false 103 | 104 | 105 | *:* 106 | 107 | META-INF/LICENSE* 108 | META-INF/NOTICE* 109 | META-INF/*.RSA 110 | META-INF/*.SF 111 | LICENSE* 112 | NOTICE* 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/ChangedLinesComputer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.OutputStream; 23 | import java.util.HashSet; 24 | import java.util.Set; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | class ChangedLinesComputer { 29 | private final Tracker tracker = new Tracker(); 30 | 31 | private final OutputStream receiver = new OutputStream() { 32 | StringBuilder sb = new StringBuilder(); 33 | 34 | @Override 35 | public void write(int b) { 36 | sb.append((char) b); 37 | if (b == '\n') { 38 | tracker.parseLine(sb.toString()); 39 | sb.setLength(0); 40 | } 41 | } 42 | }; 43 | 44 | /** 45 | * The OutputStream to pass to JGit's diff command. 46 | */ 47 | OutputStream receiver() { 48 | return receiver; 49 | } 50 | 51 | /** 52 | * From a stream of unified diff lines emitted by Git for a single file, 53 | * compute the line numbers that should be considered changed. 54 | * Example input: 55 | *
 56 |    * diff --git a/lao.txt b/lao.txt
 57 |    * index 635ef2c..7f050f2 100644
 58 |    * --- a/lao.txt
 59 |    * +++ b/lao.txt
 60 |    * @@ -1,7 +1,6 @@
 61 |    * -The Way that can be told of is not the eternal Way;
 62 |    * -The name that can be named is not the eternal name.
 63 |    *  The Nameless is the origin of Heaven and Earth;
 64 |    * -The Named is the mother of all things.
 65 |    * +The named is the mother of all things.
 66 |    * +
 67 |    *  Therefore let there always be non-being,
 68 |    *    so we may see their subtlety,
 69 |    *  And let there always be being,
 70 |    * @@ -9,3 +8,6 @@ And let there always be being,
 71 |    *  The two are the same,
 72 |    *  But after they are produced,
 73 |    *    they have different names.
 74 |    * +They both may be called deep and profound.
 75 |    * +Deeper and more profound,
 76 |    * +The door of all subtleties!names.
 77 |    * 
78 | * See also: http://www.gnu.org/software/diffutils/manual/html_node/Example-Unified.html#Example-Unified 79 | */ 80 | Set changedLines() { 81 | return tracker.changedLines(); 82 | } 83 | 84 | private static class Tracker { 85 | 86 | private static final Pattern START_LINE_IN_TARGET = Pattern.compile(" \\+(\\d+)"); 87 | 88 | private final Set changedLines = new HashSet<>(); 89 | 90 | private boolean foundStart = false; 91 | private int lineNumInTarget; 92 | 93 | private void parseLine(String line) { 94 | if (line.startsWith("@@ ")) { 95 | Matcher matcher = START_LINE_IN_TARGET.matcher(line); 96 | if (!matcher.find()) { 97 | throw new IllegalStateException("Invalid block header on line " + line); 98 | } 99 | foundStart = true; 100 | lineNumInTarget = Integer.parseInt(matcher.group(1)); 101 | } else if (foundStart) { 102 | char firstChar = line.charAt(0); 103 | if (firstChar == ' ') { 104 | lineNumInTarget++; 105 | } else if (firstChar == '+') { 106 | changedLines.add(lineNumInTarget); 107 | lineNumInTarget++; 108 | } 109 | } 110 | } 111 | 112 | Set changedLines() { 113 | return changedLines; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/GitIgnoreCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import org.sonar.api.batch.scm.IgnoreCommand; 25 | import org.sonar.api.scanner.ScannerSide; 26 | 27 | import static java.util.Objects.requireNonNull; 28 | 29 | @ScannerSide 30 | public class GitIgnoreCommand implements IgnoreCommand { 31 | 32 | private IncludedFilesRepository includedFilesRepository; 33 | 34 | @Override 35 | public void init(Path baseDir) { 36 | try { 37 | this.includedFilesRepository = new IncludedFilesRepository(baseDir); 38 | } catch (IOException e) { 39 | throw new IllegalStateException("I/O error while indexing ignored files.", e); 40 | } 41 | } 42 | 43 | @Override 44 | public boolean isIgnored(Path absolutePath) { 45 | return !requireNonNull(includedFilesRepository, "Call init first").contains(absolutePath); 46 | } 47 | 48 | @Override 49 | public void clean() { 50 | this.includedFilesRepository = null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/GitPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import org.eclipse.jgit.util.FS; 23 | import org.sonar.api.Plugin; 24 | 25 | public final class GitPlugin implements Plugin { 26 | @Override 27 | public void define(Context context) { 28 | FS.setAsyncFileStoreAttributes(true); 29 | context.addExtensions( 30 | JGitBlameCommand.class, 31 | GitScmProvider.class, 32 | GitIgnoreCommand.class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/GitScmProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.BufferedOutputStream; 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.nio.file.Path; 26 | import java.time.Instant; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Optional; 31 | import java.util.Set; 32 | import java.util.regex.Pattern; 33 | import java.util.stream.Collectors; 34 | import javax.annotation.CheckForNull; 35 | import org.eclipse.jgit.api.Git; 36 | import org.eclipse.jgit.api.errors.GitAPIException; 37 | import org.eclipse.jgit.diff.DiffAlgorithm; 38 | import org.eclipse.jgit.diff.DiffEntry; 39 | import org.eclipse.jgit.diff.DiffFormatter; 40 | import org.eclipse.jgit.diff.RawTextComparator; 41 | import org.eclipse.jgit.lib.Config; 42 | import org.eclipse.jgit.lib.ConfigConstants; 43 | import org.eclipse.jgit.lib.NullProgressMonitor; 44 | import org.eclipse.jgit.lib.ObjectReader; 45 | import org.eclipse.jgit.lib.Ref; 46 | import org.eclipse.jgit.lib.Repository; 47 | import org.eclipse.jgit.lib.RepositoryBuilder; 48 | import org.eclipse.jgit.revwalk.RevCommit; 49 | import org.eclipse.jgit.revwalk.RevWalk; 50 | import org.eclipse.jgit.revwalk.filter.RevFilter; 51 | import org.eclipse.jgit.treewalk.AbstractTreeIterator; 52 | import org.eclipse.jgit.treewalk.CanonicalTreeParser; 53 | import org.eclipse.jgit.treewalk.FileTreeIterator; 54 | import org.eclipse.jgit.treewalk.filter.PathFilter; 55 | import org.sonar.api.batch.scm.BlameCommand; 56 | import org.sonar.api.batch.scm.ScmProvider; 57 | import org.sonar.api.notifications.AnalysisWarnings; 58 | import org.sonar.api.utils.MessageException; 59 | import org.sonar.api.utils.System2; 60 | import org.sonar.api.utils.log.Logger; 61 | import org.sonar.api.utils.log.Loggers; 62 | 63 | public class GitScmProvider extends ScmProvider { 64 | 65 | private static final Logger LOG = Loggers.get(GitScmProvider.class); 66 | 67 | private final JGitBlameCommand jgitBlameCommand; 68 | private final AnalysisWarnings analysisWarnings; 69 | private final GitIgnoreCommand gitIgnoreCommand; 70 | private final System2 system2; 71 | 72 | public GitScmProvider(JGitBlameCommand jgitBlameCommand, AnalysisWarnings analysisWarnings, GitIgnoreCommand gitIgnoreCommand, System2 system2) { 73 | this.jgitBlameCommand = jgitBlameCommand; 74 | this.analysisWarnings = analysisWarnings; 75 | this.gitIgnoreCommand = gitIgnoreCommand; 76 | this.system2 = system2; 77 | } 78 | 79 | @Override 80 | public GitIgnoreCommand ignoreCommand() { 81 | return gitIgnoreCommand; 82 | } 83 | 84 | @Override 85 | public String key() { 86 | return "git"; 87 | } 88 | 89 | @Override 90 | public boolean supports(File baseDir) { 91 | RepositoryBuilder builder = new RepositoryBuilder().findGitDir(baseDir); 92 | return builder.getGitDir() != null; 93 | } 94 | 95 | @Override 96 | public BlameCommand blameCommand() { 97 | return this.jgitBlameCommand; 98 | } 99 | 100 | @CheckForNull 101 | @Override 102 | public Set branchChangedFiles(String targetBranchName, Path rootBaseDir) { 103 | try (Repository repo = buildRepo(rootBaseDir)) { 104 | Ref targetRef = resolveTargetRef(targetBranchName, repo); 105 | if (targetRef == null) { 106 | analysisWarnings.addUnique(String.format("Could not find ref '%s' in refs/heads, refs/remotes/upstream or refs/remotes/origin. " 107 | + "You may see unexpected issues and changes. " 108 | + "Please make sure to fetch this ref before pull request analysis.", targetBranchName)); 109 | return null; 110 | } 111 | 112 | if (isDiffAlgoInvalid(repo.getConfig())) { 113 | LOG.warn("The diff algorithm configured in git is not supported. " 114 | + "No information regarding changes in the branch will be collected, which can lead to unexpected results."); 115 | return null; 116 | } 117 | 118 | Optional mergeBaseCommit = findMergeBase(repo, targetRef); 119 | if (!mergeBaseCommit.isPresent()) { 120 | LOG.warn("No merge base found between HEAD and " + targetRef.getName()); 121 | return null; 122 | } 123 | AbstractTreeIterator mergeBaseTree = prepareTreeParser(repo, mergeBaseCommit.get()); 124 | 125 | // we compare a commit with HEAD, so no point ignoring line endings (it will be whatever is committed) 126 | try (Git git = newGit(repo)) { 127 | List diffEntries = git.diff() 128 | .setShowNameAndStatusOnly(true) 129 | .setOldTree(mergeBaseTree) 130 | .setNewTree(prepareNewTree(repo)) 131 | .call(); 132 | 133 | return diffEntries.stream() 134 | .filter(diffEntry -> diffEntry.getChangeType() == DiffEntry.ChangeType.ADD || diffEntry.getChangeType() == DiffEntry.ChangeType.MODIFY) 135 | .map(diffEntry -> repo.getWorkTree().toPath().resolve(diffEntry.getNewPath())) 136 | .collect(Collectors.toSet()); 137 | } 138 | } catch (IOException | GitAPIException e) { 139 | LOG.warn(e.getMessage(), e); 140 | } 141 | return null; 142 | } 143 | 144 | @CheckForNull 145 | @Override 146 | public Map> branchChangedLines(String targetBranchName, Path projectBaseDir, Set changedFiles) { 147 | try (Repository repo = buildRepo(projectBaseDir)) { 148 | Ref targetRef = resolveTargetRef(targetBranchName, repo); 149 | if (targetRef == null) { 150 | analysisWarnings.addUnique(String.format("Could not find ref '%s' in refs/heads, refs/remotes/upstream or refs/remotes/origin. " 151 | + "You may see unexpected issues and changes. " 152 | + "Please make sure to fetch this ref before pull request analysis.", targetBranchName)); 153 | return null; 154 | } 155 | 156 | if (isDiffAlgoInvalid(repo.getConfig())) { 157 | // we already print a warning when branchChangedFiles is called 158 | return null; 159 | } 160 | 161 | // force ignore different line endings when comparing a commit with the workspace 162 | repo.getConfig().setBoolean("core", null, "autocrlf", true); 163 | 164 | Optional mergeBaseCommit = findMergeBase(repo, targetRef); 165 | if (!mergeBaseCommit.isPresent()) { 166 | LOG.warn("No merge base found between HEAD and " + targetRef.getName()); 167 | return null; 168 | } 169 | 170 | Map> changedLines = new HashMap<>(); 171 | Path repoRootDir = repo.getDirectory().toPath().getParent(); 172 | 173 | for (Path path : changedFiles) { 174 | collectChangedLines(repo, mergeBaseCommit.get(), changedLines, repoRootDir, path); 175 | } 176 | return changedLines; 177 | } catch (Exception e) { 178 | LOG.warn("Failed to get changed lines from git", e); 179 | } 180 | return null; 181 | } 182 | 183 | private void collectChangedLines(Repository repo, RevCommit mergeBaseCommit, Map> changedLines, Path repoRootDir, Path changedFile) { 184 | ChangedLinesComputer computer = new ChangedLinesComputer(); 185 | 186 | try (DiffFormatter diffFmt = new DiffFormatter(new BufferedOutputStream(computer.receiver()))) { 187 | // copied from DiffCommand so that we can use a custom DiffFormatter which ignores white spaces. 188 | diffFmt.setRepository(repo); 189 | diffFmt.setProgressMonitor(NullProgressMonitor.INSTANCE); 190 | diffFmt.setDiffComparator(RawTextComparator.WS_IGNORE_ALL); 191 | diffFmt.setPathFilter(PathFilter.create(toGitPath(repoRootDir.relativize(changedFile).toString()))); 192 | 193 | AbstractTreeIterator mergeBaseTree = prepareTreeParser(repo, mergeBaseCommit); 194 | List diffEntries = diffFmt.scan(mergeBaseTree, new FileTreeIterator(repo)); 195 | diffFmt.format(diffEntries); 196 | diffFmt.flush(); 197 | diffEntries.stream() 198 | .filter(diffEntry -> diffEntry.getChangeType() == DiffEntry.ChangeType.ADD || diffEntry.getChangeType() == DiffEntry.ChangeType.MODIFY) 199 | .findAny() 200 | .ifPresent(diffEntry -> changedLines.put(changedFile, computer.changedLines())); 201 | } catch (Exception e) { 202 | LOG.warn("Failed to get changed lines from git for file " + changedFile, e); 203 | } 204 | } 205 | 206 | /** 207 | * This method will override API in SQ 8.4 208 | */ 209 | @CheckForNull 210 | public Instant forkDate(String referenceBranchName, Path projectBaseDir) { 211 | try (Repository repo = buildRepo(projectBaseDir)) { 212 | Ref targetRef = resolveTargetRef(referenceBranchName, repo); 213 | if (targetRef == null) { 214 | LOG.warn("Branch '{}' not found in git", referenceBranchName); 215 | return null; 216 | } 217 | 218 | if (isDiffAlgoInvalid(repo.getConfig())) { 219 | LOG.warn("The diff algorithm configured in git is not supported. " 220 | + "No information regarding changes in the branch will be collected, which can lead to unexpected results."); 221 | return null; 222 | } 223 | 224 | Optional mergeBaseCommit = findMergeBase(repo, targetRef); 225 | if (!mergeBaseCommit.isPresent()) { 226 | LOG.warn("No fork point found between HEAD and " + targetRef.getName()); 227 | return null; 228 | } 229 | 230 | return Instant.ofEpochSecond(mergeBaseCommit.get().getCommitTime()); 231 | } catch (Exception e) { 232 | LOG.warn("Failed to find fork point with git", e); 233 | } 234 | 235 | return null; 236 | } 237 | 238 | private static String toGitPath(String path) { 239 | return path.replaceAll(Pattern.quote(File.separator), "/"); 240 | } 241 | 242 | @CheckForNull 243 | private Ref resolveTargetRef(String targetBranchName, Repository repo) throws IOException { 244 | String localRef = "refs/heads/" + targetBranchName; 245 | String remoteRef = "refs/remotes/origin/" + targetBranchName; 246 | String upstreamRef = "refs/remotes/upstream/" + targetBranchName; 247 | 248 | Ref targetRef; 249 | // Because circle ci destroys the local reference to master, try to load remote ref first. 250 | // https://discuss.circleci.com/t/git-checkout-of-a-branch-destroys-local-reference-to-master/23781 251 | if (runningOnCircleCI()) { 252 | targetRef = getFirstExistingRef(repo, remoteRef, localRef, upstreamRef); 253 | } else { 254 | targetRef = getFirstExistingRef(repo, localRef, remoteRef, upstreamRef); 255 | } 256 | 257 | if (targetRef == null) { 258 | LOG.warn("Could not find ref: {} in refs/heads, refs/remotes/upstream or refs/remotes/origin", targetBranchName); 259 | } 260 | 261 | return targetRef; 262 | } 263 | 264 | @CheckForNull 265 | private static Ref getFirstExistingRef(Repository repo, String... refs) throws IOException { 266 | Ref targetRef = null; 267 | for (String ref : refs) { 268 | targetRef = repo.exactRef(ref); 269 | if (targetRef != null) { 270 | break; 271 | } 272 | } 273 | return targetRef; 274 | } 275 | 276 | private boolean runningOnCircleCI() { 277 | return "true".equals(system2.envVariable("CIRCLECI")); 278 | } 279 | 280 | @Override 281 | public Path relativePathFromScmRoot(Path path) { 282 | RepositoryBuilder builder = getVerifiedRepositoryBuilder(path); 283 | return builder.getGitDir().toPath().getParent().relativize(path); 284 | } 285 | 286 | @Override 287 | @CheckForNull 288 | public String revisionId(Path path) { 289 | RepositoryBuilder builder = getVerifiedRepositoryBuilder(path); 290 | try { 291 | Ref head = getHead(builder.build()); 292 | if (head == null || head.getObjectId() == null) { 293 | // can happen on fresh, empty repos 294 | return null; 295 | } 296 | return head.getObjectId().getName(); 297 | } catch (IOException e) { 298 | throw new IllegalStateException("I/O error while getting revision ID for path: " + path, e); 299 | } 300 | } 301 | 302 | private static boolean isDiffAlgoInvalid(Config cfg) { 303 | try { 304 | DiffAlgorithm.getAlgorithm(cfg.getEnum( 305 | ConfigConstants.CONFIG_DIFF_SECTION, null, 306 | ConfigConstants.CONFIG_KEY_ALGORITHM, 307 | DiffAlgorithm.SupportedAlgorithm.HISTOGRAM)); 308 | return false; 309 | } catch (IllegalArgumentException e) { 310 | return true; 311 | } 312 | } 313 | 314 | private static AbstractTreeIterator prepareNewTree(Repository repo) throws IOException { 315 | CanonicalTreeParser treeParser = new CanonicalTreeParser(); 316 | try (ObjectReader objectReader = repo.newObjectReader()) { 317 | Ref head = getHead(repo); 318 | if (head == null) { 319 | throw new IOException("HEAD reference not found"); 320 | } 321 | treeParser.reset(objectReader, repo.parseCommit(head.getObjectId()).getTree()); 322 | } 323 | return treeParser; 324 | } 325 | 326 | @CheckForNull 327 | private static Ref getHead(Repository repo) throws IOException { 328 | return repo.exactRef("HEAD"); 329 | } 330 | 331 | private static Optional findMergeBase(Repository repo, Ref targetRef) throws IOException { 332 | try (RevWalk walk = new RevWalk(repo)) { 333 | Ref head = getHead(repo); 334 | if (head == null) { 335 | throw new IOException("HEAD reference not found"); 336 | } 337 | 338 | walk.markStart(walk.parseCommit(targetRef.getObjectId())); 339 | walk.markStart(walk.parseCommit(head.getObjectId())); 340 | walk.setRevFilter(RevFilter.MERGE_BASE); 341 | RevCommit next = walk.next(); 342 | if (next == null) { 343 | return Optional.empty(); 344 | } 345 | RevCommit base = walk.parseCommit(next); 346 | walk.dispose(); 347 | LOG.debug("Merge base sha1: {}", base.getName()); 348 | return Optional.of(base); 349 | } 350 | } 351 | 352 | AbstractTreeIterator prepareTreeParser(Repository repo, RevCommit commit) throws IOException { 353 | CanonicalTreeParser treeParser = new CanonicalTreeParser(); 354 | try (ObjectReader objectReader = repo.newObjectReader()) { 355 | treeParser.reset(objectReader, commit.getTree()); 356 | } 357 | return treeParser; 358 | } 359 | 360 | Git newGit(Repository repo) { 361 | return new Git(repo); 362 | } 363 | 364 | Repository buildRepo(Path basedir) throws IOException { 365 | return getVerifiedRepositoryBuilder(basedir).build(); 366 | } 367 | 368 | static RepositoryBuilder getVerifiedRepositoryBuilder(Path basedir) { 369 | RepositoryBuilder builder = new RepositoryBuilder() 370 | .findGitDir(basedir.toFile()) 371 | .setMustExist(true); 372 | 373 | if (builder.getGitDir() == null) { 374 | throw MessageException.of("Not inside a Git work tree: " + basedir); 375 | } 376 | return builder; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/GitThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.util.concurrent.ForkJoinPool; 23 | import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; 24 | import java.util.concurrent.ForkJoinWorkerThread; 25 | 26 | public class GitThreadFactory implements ForkJoinWorkerThreadFactory { 27 | private static final String NAME_PREFIX = "git-scm-"; 28 | private int i = 0; 29 | 30 | @Override 31 | public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 32 | ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); 33 | thread.setName(NAME_PREFIX + i++); 34 | return thread; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/IncludedFilesRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import java.util.HashSet; 25 | import java.util.Set; 26 | import org.eclipse.jgit.lib.Repository; 27 | import org.eclipse.jgit.treewalk.FileTreeIterator; 28 | import org.eclipse.jgit.treewalk.TreeWalk; 29 | import org.eclipse.jgit.treewalk.WorkingTreeIterator; 30 | import org.eclipse.jgit.treewalk.filter.PathFilterGroup; 31 | import org.sonar.api.utils.log.Logger; 32 | import org.sonar.api.utils.log.Loggers; 33 | 34 | public class IncludedFilesRepository { 35 | 36 | private static final Logger LOG = Loggers.get(IncludedFilesRepository.class); 37 | private final Set includedFiles = new HashSet<>(); 38 | 39 | public IncludedFilesRepository(Path baseDir) throws IOException { 40 | indexFiles(baseDir); 41 | LOG.debug("{} non excluded files in this Git repository", includedFiles.size()); 42 | } 43 | 44 | public boolean contains(Path absolutePath) { 45 | return includedFiles.contains(absolutePath); 46 | } 47 | 48 | private void indexFiles(Path baseDir) throws IOException { 49 | try (Repository repo = JGitUtils.buildRepository(baseDir)) { 50 | Path workTreeRoot = repo.getWorkTree().toPath(); 51 | FileTreeIterator workingTreeIt = new FileTreeIterator(repo); 52 | try (TreeWalk treeWalk = new TreeWalk(repo)) { 53 | treeWalk.setRecursive(true); 54 | if (!baseDir.equals(workTreeRoot)) { 55 | Path relativeBaseDir = workTreeRoot.relativize(baseDir); 56 | treeWalk.setFilter(PathFilterGroup.createFromStrings(relativeBaseDir.toString().replace('\\', '/'))); 57 | } 58 | treeWalk.addTree(workingTreeIt); 59 | while (treeWalk.next()) { 60 | 61 | WorkingTreeIterator workingTreeIterator = treeWalk 62 | .getTree(0, WorkingTreeIterator.class); 63 | 64 | if (!workingTreeIterator.isEntryIgnored()) { 65 | includedFiles.add(workTreeRoot.resolve(treeWalk.getPathString())); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/JGitBlameCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.File; 23 | import java.nio.file.Files; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.concurrent.ForkJoinPool; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.stream.Stream; 29 | import java.util.stream.StreamSupport; 30 | import org.eclipse.jgit.api.Git; 31 | import org.eclipse.jgit.blame.BlameResult; 32 | import org.eclipse.jgit.diff.RawTextComparator; 33 | import org.eclipse.jgit.lib.Repository; 34 | import org.sonar.api.batch.fs.InputFile; 35 | import org.sonar.api.batch.scm.BlameCommand; 36 | import org.sonar.api.batch.scm.BlameLine; 37 | import org.sonar.api.notifications.AnalysisWarnings; 38 | import org.sonar.api.scan.filesystem.PathResolver; 39 | import org.sonar.api.utils.log.Logger; 40 | import org.sonar.api.utils.log.Loggers; 41 | 42 | public class JGitBlameCommand extends BlameCommand { 43 | 44 | private static final Logger LOG = Loggers.get(JGitBlameCommand.class); 45 | 46 | private final PathResolver pathResolver; 47 | private final AnalysisWarnings analysisWarnings; 48 | 49 | public JGitBlameCommand(PathResolver pathResolver, AnalysisWarnings analysisWarnings) { 50 | this.pathResolver = pathResolver; 51 | this.analysisWarnings = analysisWarnings; 52 | } 53 | 54 | @Override 55 | public void blame(BlameInput input, BlameOutput output) { 56 | File basedir = input.fileSystem().baseDir(); 57 | try (Repository repo = JGitUtils.buildRepository(basedir.toPath()); Git git = Git.wrap(repo)) { 58 | File gitBaseDir = repo.getWorkTree(); 59 | 60 | if (cloneIsInvalid(gitBaseDir)) { 61 | return; 62 | } 63 | 64 | Stream stream = StreamSupport.stream(input.filesToBlame().spliterator(), true); 65 | ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory(), null, false); 66 | forkJoinPool.submit(() -> stream.forEach(inputFile -> blame(output, git, gitBaseDir, inputFile))); 67 | try { 68 | forkJoinPool.shutdown(); 69 | forkJoinPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); 70 | } catch (InterruptedException e) { 71 | LOG.info("Git blame interrupted"); 72 | } 73 | } 74 | } 75 | 76 | private boolean cloneIsInvalid(File gitBaseDir) { 77 | if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/objects/info/alternates"))) { 78 | LOG.info("This git repository references another local repository which is not well supported. SCM information might be missing for some files. " 79 | + "You can avoid borrow objects from another local repository by not using --reference or --shared when cloning it."); 80 | } 81 | 82 | if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/shallow"))) { 83 | LOG.warn("Shallow clone detected, no blame information will be provided. " 84 | + "You can convert to non-shallow with 'git fetch --unshallow'."); 85 | analysisWarnings.addUnique("Shallow clone detected during the analysis. " 86 | + "Some files will miss SCM information. This will affect features like auto-assignment of issues. " 87 | + "Please configure your build to disable shallow clone."); 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | private void blame(BlameOutput output, Git git, File gitBaseDir, InputFile inputFile) { 95 | String filename = pathResolver.relativePath(gitBaseDir, inputFile.file()); 96 | LOG.debug("Blame file {}", filename); 97 | BlameResult blameResult; 98 | try { 99 | blameResult = git.blame() 100 | // Equivalent to -w command line option 101 | .setTextComparator(RawTextComparator.WS_IGNORE_ALL) 102 | .setFilePath(filename).call(); 103 | } catch (Exception e) { 104 | throw new IllegalStateException("Unable to blame file " + inputFile.relativePath(), e); 105 | } 106 | List lines = new ArrayList<>(); 107 | if (blameResult == null) { 108 | LOG.debug("Unable to blame file {}. It is probably a symlink.", inputFile.relativePath()); 109 | return; 110 | } 111 | for (int i = 0; i < blameResult.getResultContents().size(); i++) { 112 | if (blameResult.getSourceAuthor(i) == null || blameResult.getSourceCommit(i) == null) { 113 | LOG.debug("Unable to blame file {}. No blame info at line {}. Is file committed? [Author: {} Source commit: {}]", inputFile.relativePath(), i + 1, 114 | blameResult.getSourceAuthor(i), blameResult.getSourceCommit(i)); 115 | return; 116 | } 117 | lines.add(new BlameLine() 118 | .date(blameResult.getSourceCommitter(i).getWhen()) 119 | .revision(blameResult.getSourceCommit(i).getName()) 120 | .author(blameResult.getSourceAuthor(i).getEmailAddress())); 121 | } 122 | if (lines.size() == inputFile.lines() - 1) { 123 | // SONARPLUGINS-3097 Git do not report blame on last empty line 124 | lines.add(lines.get(lines.size() - 1)); 125 | } 126 | output.blameResult(inputFile, lines); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/JGitUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import org.eclipse.jgit.lib.ObjectReader; 25 | import org.eclipse.jgit.lib.Repository; 26 | 27 | public class JGitUtils { 28 | 29 | private JGitUtils() { 30 | } 31 | 32 | public static Repository buildRepository(Path basedir) { 33 | try { 34 | Repository repo = GitScmProvider.getVerifiedRepositoryBuilder(basedir).build(); 35 | try (ObjectReader objReader = repo.getObjectDatabase().newReader()) { 36 | // SONARSCGIT-2 Force initialization of shallow commits to avoid later concurrent modification issue 37 | objReader.getShallowCommits(); 38 | return repo; 39 | } 40 | } catch (IOException e) { 41 | throw new IllegalStateException("Unable to open Git repository", e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/main/java/org/sonarsource/scm/git/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 22 | 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/ChangedLinesComputerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.IOException; 23 | import java.io.OutputStreamWriter; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import org.junit.rules.ExpectedException; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | public class ChangedLinesComputerTest { 31 | @Rule 32 | public ExpectedException exception = ExpectedException.none(); 33 | private final ChangedLinesComputer underTest = new ChangedLinesComputer(); 34 | 35 | @Test 36 | public void do_not_count_deleted_line() throws IOException { 37 | String example = "diff --git a/file-b1.xoo b/file-b1.xoo\n" 38 | + "index 0000000..c2a9048\n" 39 | + "--- a/foo\n" 40 | + "+++ b/bar\n" 41 | + "@@ -1 +0,0 @@\n" 42 | + "-deleted line\n"; 43 | 44 | printDiff(example); 45 | assertThat(underTest.changedLines()).isEmpty(); 46 | } 47 | 48 | @Test 49 | public void count_single_added_line() throws IOException { 50 | String example = "diff --git a/file-b1.xoo b/file-b1.xoo\n" 51 | + "index 0000000..c2a9048\n" 52 | + "--- a/foo\n" 53 | + "+++ b/bar\n" 54 | + "@@ -0,0 +1 @@\n" 55 | + "+added line\n"; 56 | 57 | printDiff(example); 58 | assertThat(underTest.changedLines()).containsExactly(1); 59 | } 60 | 61 | @Test 62 | public void count_multiple_added_lines() throws IOException { 63 | String example = "diff --git a/file-b1.xoo b/file-b1.xoo\n" 64 | + "index 0000000..c2a9048\n" 65 | + "--- a/foo\n" 66 | + "+++ b/bar\n" 67 | + "@@ -1 +1,3 @@\n" 68 | + " unchanged line\n" 69 | + "+added line 1\n" 70 | + "+added line 2\n"; 71 | 72 | printDiff(example); 73 | assertThat(underTest.changedLines()).containsExactly(2, 3); 74 | } 75 | 76 | @Test 77 | public void compute_from_multiple_hunks() throws IOException { 78 | String example = "diff --git a/lao b/lao\n" 79 | + "index 635ef2c..5af88a8 100644\n" 80 | + "--- a/lao\n" 81 | + "+++ b/lao\n" 82 | + "@@ -1,7 +1,6 @@\n" 83 | + "-The Way that can be told of is not the eternal Way;\n" 84 | + "-The name that can be named is not the eternal name.\n" 85 | + " The Nameless is the origin of Heaven and Earth;\n" 86 | + "-The Named is the mother of all things.\n" 87 | + "+The named is the mother of all things.\n" 88 | + "+\n" 89 | + " Therefore let there always be non-being,\n" 90 | + " so we may see their subtlety,\n" 91 | + " And let there always be being,\n" 92 | + "@@ -9,3 +8,6 @@ And let there always be being,\n" 93 | + " The two are the same,\n" 94 | + " But after they are produced,\n" 95 | + " they have different names.\n" 96 | + "+They both may be called deep and profound.\n" 97 | + "+Deeper and more profound,\n" 98 | + "+The door of all subtleties!\n"; 99 | printDiff(example); 100 | assertThat(underTest.changedLines()).containsExactly(2, 3, 11, 12, 13); 101 | } 102 | 103 | @Test 104 | public void compute_from_multiple_hunks_with_extra_header_lines() throws IOException { 105 | String example = "diff --git a/lao b/lao\n" 106 | + "new file mode 100644\n" 107 | + "whatever " 108 | + "other " 109 | + "surprise header lines git might throw at us...\n" 110 | + "index 635ef2c..5af88a8 100644\n" 111 | + "--- a/lao\n" 112 | + "+++ b/lao\n" 113 | + "@@ -1,7 +1,6 @@\n" 114 | + "-The Way that can be told of is not the eternal Way;\n" 115 | + "-The name that can be named is not the eternal name.\n" 116 | + " The Nameless is the origin of Heaven and Earth;\n" 117 | + "-The Named is the mother of all things.\n" 118 | + "+The named is the mother of all things.\n" 119 | + "+\n" 120 | + " Therefore let there always be non-being,\n" 121 | + " so we may see their subtlety,\n" 122 | + " And let there always be being,\n" 123 | + "@@ -9,3 +8,6 @@ And let there always be being,\n" 124 | + " The two are the same,\n" 125 | + " But after they are produced,\n" 126 | + " they have different names.\n" 127 | + "+They both may be called deep and profound.\n" 128 | + "+Deeper and more profound,\n" 129 | + "+The door of all subtleties!\n"; 130 | printDiff(example); 131 | assertThat(underTest.changedLines()).containsExactly(2, 3, 11, 12, 13); 132 | } 133 | 134 | @Test 135 | public void throw_exception_invalid_start_line_format() throws IOException { 136 | String example = "diff --git a/file-b1.xoo b/file-b1.xoo\n" 137 | + "index 0000000..c2a9048\n" 138 | + "--- a/foo\n" 139 | + "+++ b/bar\n" 140 | + "@@ -1 +x1,3 @@\n" 141 | + " unchanged line\n" 142 | + "+added line 1\n" 143 | + "+added line 2\n"; 144 | 145 | exception.expect(IllegalStateException.class); 146 | printDiff(example); 147 | } 148 | 149 | private void printDiff(String unifiedDiff) throws IOException { 150 | try (OutputStreamWriter writer = new OutputStreamWriter(underTest.receiver())) { 151 | writer.write(unifiedDiff); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/GitIgnoreCommandTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.nio.file.StandardOpenOption; 28 | import java.util.Arrays; 29 | import org.eclipse.jgit.api.Git; 30 | import org.junit.Rule; 31 | import org.junit.Test; 32 | import org.junit.rules.TemporaryFolder; 33 | import org.sonar.api.utils.log.LogTester; 34 | import org.sonar.api.utils.log.LoggerLevel; 35 | 36 | import static org.assertj.core.api.Assertions.assertThat; 37 | import static org.sonarsource.scm.git.Utils.javaUnzip; 38 | 39 | public class GitIgnoreCommandTest { 40 | 41 | @Rule 42 | public LogTester logTester = new LogTester(); 43 | 44 | @Rule 45 | public TemporaryFolder temp = new TemporaryFolder(); 46 | 47 | @Test 48 | public void ignored_files_should_match_files_ignored_by_git() throws IOException { 49 | Path projectDir = temp.newFolder().toPath(); 50 | javaUnzip(new File("test-repos/ignore-git.zip"), projectDir.toFile()); 51 | 52 | Path baseDir = projectDir.resolve("ignore-git"); 53 | GitIgnoreCommand underTest = new GitIgnoreCommand(); 54 | underTest.init(baseDir); 55 | 56 | assertThat(underTest.isIgnored(baseDir.resolve(".gitignore"))).isFalse(); 57 | assertThat(underTest.isIgnored(baseDir.resolve("pom.xml"))).isFalse(); 58 | assertThat(underTest.isIgnored(baseDir.resolve("src/main/java/org/dummy/.gitignore"))).isFalse(); 59 | assertThat(underTest.isIgnored(baseDir.resolve("src/main/java/org/dummy/AnotherDummy.java"))).isFalse(); 60 | assertThat(underTest.isIgnored(baseDir.resolve("src/test/java/org/dummy/AnotherDummyTest.java"))).isFalse(); 61 | 62 | assertThat(underTest.isIgnored(baseDir.resolve("src/main/java/org/dummy/Dummy.java"))).isTrue(); 63 | assertThat(underTest.isIgnored(baseDir.resolve("target"))).isTrue(); 64 | } 65 | 66 | @Test 67 | public void test_pattern_on_deep_repo() throws Exception { 68 | Path projectDir = temp.newFolder().toPath(); 69 | Git.init().setDirectory(projectDir.toFile()).call(); 70 | 71 | Files.write(projectDir.resolve(".gitignore"), Arrays.asList("**/*.java"), StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); 72 | int child_folders_per_folder = 2; 73 | int folder_depth = 10; 74 | createDeepFolderStructure(projectDir, child_folders_per_folder, 0, folder_depth); 75 | 76 | logTester.setLevel(LoggerLevel.DEBUG); 77 | 78 | GitIgnoreCommand underTest = new GitIgnoreCommand(); 79 | underTest.init(projectDir); 80 | 81 | assertThat(underTest 82 | .isIgnored(projectDir.resolve("folder_0_0/folder_1_0/folder_2_0/folder_3_0/folder_4_0/folder_5_0/folder_6_0/folder_7_0/folder_8_0/folder_9_0/Foo.java"))) 83 | .isTrue(); 84 | assertThat(underTest 85 | .isIgnored(projectDir.resolve("folder_0_0/folder_1_0/folder_2_0/folder_3_0/folder_4_0/folder_5_0/folder_6_0/folder_7_0/folder_8_0/folder_9_0/Foo.php"))) 86 | .isFalse(); 87 | 88 | int expectedIncludedFiles = (int) Math.pow(child_folders_per_folder, folder_depth) + 1; // The .gitignore file is indexed 89 | assertThat(logTester.logs(LoggerLevel.DEBUG)).contains(expectedIncludedFiles + " non excluded files in this Git repository"); 90 | } 91 | 92 | @Test 93 | public void dont_index_files_outside_basedir() throws Exception { 94 | Path repoRoot = temp.newFolder().toPath(); 95 | Git.init().setDirectory(repoRoot.toFile()).call(); 96 | 97 | Files.write(repoRoot.resolve(".gitignore"), Arrays.asList("**/*.java"), StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); 98 | int child_folders_per_folder = 2; 99 | int folder_depth = 10; 100 | createDeepFolderStructure(repoRoot, child_folders_per_folder, 0, folder_depth); 101 | 102 | logTester.setLevel(LoggerLevel.DEBUG); 103 | 104 | GitIgnoreCommand underTest = new GitIgnoreCommand(); 105 | // Define project baseDir as folder_0_0 so that folder_0_1 is excluded 106 | Path projectBasedir = repoRoot.resolve("folder_0_0"); 107 | underTest.init(projectBasedir); 108 | 109 | assertThat(underTest 110 | .isIgnored(projectBasedir.resolve("folder_1_0/folder_2_0/folder_3_0/folder_4_0/folder_5_0/folder_6_0/folder_7_0/folder_8_0/folder_9_0/Foo.php"))) 111 | .isFalse(); 112 | assertThat(underTest 113 | .isIgnored(repoRoot.resolve("folder_0_1/folder_1_0/folder_2_0/folder_3_0/folder_4_0/folder_5_0/folder_6_0/folder_7_0/folder_8_0/folder_9_0/Foo.php"))) 114 | .isTrue(); 115 | 116 | int expectedIncludedFiles = (int) Math.pow(child_folders_per_folder, folder_depth - 1); 117 | assertThat(logTester.logs(LoggerLevel.DEBUG)).contains(expectedIncludedFiles + " non excluded files in this Git repository"); 118 | } 119 | 120 | private void createDeepFolderStructure(Path current, int childCount, int currentDepth, int maxDepth) throws IOException { 121 | if (currentDepth >= maxDepth) { 122 | Path javaFile = current.resolve("Foo.java"); 123 | Path phpFile = current.resolve("Foo.php"); 124 | if (!Files.exists(phpFile)) { 125 | Files.createFile(phpFile); 126 | } 127 | if (!Files.exists(javaFile)) { 128 | Files.createFile(javaFile); 129 | } 130 | return; 131 | } 132 | for (int j = 0; j < childCount; j++) { 133 | Path newPath = current.resolve("folder_" + currentDepth + "_" + j); 134 | if (!Files.exists(newPath)) { 135 | Files.createDirectory(newPath); 136 | } 137 | createDeepFolderStructure(newPath, childCount, currentDepth + 1, maxDepth); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/GitPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import org.junit.Test; 23 | import org.sonar.api.Plugin; 24 | import org.sonar.api.SonarRuntime; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | import static org.mockito.Mockito.mock; 28 | 29 | public class GitPluginTest { 30 | 31 | @Test 32 | public void getExtensions() { 33 | SonarRuntime runtime = mock(SonarRuntime.class); 34 | Plugin.Context context = new Plugin.Context(runtime); 35 | new GitPlugin().define(context); 36 | assertThat(context.getExtensions()).hasSize(3); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/GitScmProviderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.nio.file.Paths; 28 | import java.nio.file.StandardOpenOption; 29 | import java.time.Instant; 30 | import java.time.temporal.ChronoUnit; 31 | import java.util.Arrays; 32 | import java.util.Collections; 33 | import java.util.Date; 34 | import java.util.HashSet; 35 | import java.util.LinkedHashSet; 36 | import java.util.List; 37 | import java.util.Map; 38 | import java.util.Random; 39 | import java.util.Set; 40 | import java.util.TimeZone; 41 | import java.util.concurrent.atomic.AtomicInteger; 42 | import org.eclipse.jgit.api.DiffCommand; 43 | import org.eclipse.jgit.api.Git; 44 | import org.eclipse.jgit.api.errors.GitAPIException; 45 | import org.eclipse.jgit.lib.ObjectId; 46 | import org.eclipse.jgit.lib.PersonIdent; 47 | import org.eclipse.jgit.lib.RefDatabase; 48 | import org.eclipse.jgit.lib.Repository; 49 | import org.eclipse.jgit.revwalk.RevCommit; 50 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder; 51 | import org.eclipse.jgit.treewalk.AbstractTreeIterator; 52 | import org.junit.Before; 53 | import org.junit.Rule; 54 | import org.junit.Test; 55 | import org.junit.rules.ExpectedException; 56 | import org.junit.rules.TemporaryFolder; 57 | import org.sonar.api.internal.google.common.collect.ImmutableMap; 58 | import org.sonar.api.internal.google.common.collect.ImmutableSet; 59 | import org.sonar.api.notifications.AnalysisWarnings; 60 | import org.sonar.api.scan.filesystem.PathResolver; 61 | import org.sonar.api.utils.MessageException; 62 | import org.sonar.api.utils.System2; 63 | 64 | import static java.util.Collections.emptySet; 65 | import static org.assertj.core.api.Assertions.assertThat; 66 | import static org.assertj.core.data.MapEntry.entry; 67 | import static org.mockito.ArgumentMatchers.any; 68 | import static org.mockito.ArgumentMatchers.anyBoolean; 69 | import static org.mockito.Mockito.mock; 70 | import static org.mockito.Mockito.verify; 71 | import static org.mockito.Mockito.verifyZeroInteractions; 72 | import static org.mockito.Mockito.when; 73 | import static org.sonarsource.scm.git.Utils.javaUnzip; 74 | 75 | public class GitScmProviderTest { 76 | 77 | // Sample content for unified diffs 78 | // http://www.gnu.org/software/diffutils/manual/html_node/Example-Unified.html#Example-Unified 79 | private static final String CONTENT_LAO = "The Way that can be told of is not the eternal Way;\n" 80 | + "The name that can be named is not the eternal name.\n" 81 | + "The Nameless is the origin of Heaven and Earth;\n" 82 | + "The Named is the mother of all things.\n" 83 | + "Therefore let there always be non-being,\n" 84 | + " so we may see their subtlety,\n" 85 | + "And let there always be being,\n" 86 | + " so we may see their outcome.\n" 87 | + "The two are the same,\n" 88 | + "But after they are produced,\n" 89 | + " they have different names.\n"; 90 | 91 | private static final String CONTENT_TZU = "The Nameless is the origin of Heaven and Earth;\n" 92 | + "The named is the mother of all things.\n" 93 | + "\n" 94 | + "Therefore let there always be non-being,\n" 95 | + " so we may see their subtlety,\n" 96 | + "And let there always be being,\n" 97 | + " so we may see their outcome.\n" 98 | + "The two are the same,\n" 99 | + "But after they are produced,\n" 100 | + " they have different names.\n" 101 | + "They both may be called deep and profound.\n" 102 | + "Deeper and more profound,\n" 103 | + "The door of all subtleties!"; 104 | 105 | @Rule 106 | public TemporaryFolder temp = new TemporaryFolder(); 107 | 108 | @Rule 109 | public ExpectedException thrown = ExpectedException.none(); 110 | 111 | private GitIgnoreCommand gitIgnoreCommand = mock(GitIgnoreCommand.class); 112 | private static final Random random = new Random(); 113 | private static final System2 system2 = mock(System2.class); 114 | 115 | private Path worktree; 116 | private Git git; 117 | private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); 118 | 119 | @Before 120 | public void before() throws IOException, GitAPIException { 121 | worktree = temp.newFolder().toPath(); 122 | Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); 123 | repo.create(); 124 | 125 | git = new Git(repo); 126 | 127 | createAndCommitFile("file-in-first-commit.xoo"); 128 | } 129 | 130 | @Test 131 | public void sanityCheck() { 132 | assertThat(newGitScmProvider().key()).isEqualTo("git"); 133 | } 134 | 135 | @Test 136 | public void returnImplem() { 137 | JGitBlameCommand jblameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings); 138 | GitScmProvider gitScmProvider = new GitScmProvider(jblameCommand, analysisWarnings, gitIgnoreCommand, system2); 139 | 140 | assertThat(gitScmProvider.blameCommand()).isEqualTo(jblameCommand); 141 | } 142 | 143 | /** 144 | * SONARSCGIT-47 145 | */ 146 | @Test 147 | public void branchChangedFiles_should_not_crash_if_branches_have_no_common_ancestors() throws GitAPIException, IOException { 148 | String fileName = "file-in-first-commit.xoo"; 149 | String renamedName = "file-renamed.xoo"; 150 | git.checkout().setOrphan(true).setName("b1").call(); 151 | 152 | Path file = worktree.resolve(fileName); 153 | Path renamed = file.resolveSibling(renamedName); 154 | addLineToFile(fileName, 1); 155 | 156 | Files.move(file, renamed); 157 | git.rm().addFilepattern(fileName).call(); 158 | commit(renamedName); 159 | 160 | Set files = newScmProvider().branchChangedFiles("master", worktree); 161 | 162 | // no shared history, so no diff 163 | assertThat(files).isNull(); 164 | } 165 | 166 | @Test 167 | public void testAutodetection() throws IOException { 168 | File baseDirEmpty = temp.newFolder(); 169 | assertThat(newGitScmProvider().supports(baseDirEmpty)).isFalse(); 170 | 171 | File projectDir = temp.newFolder(); 172 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 173 | File baseDir = new File(projectDir, "dummy-git"); 174 | assertThat(newScmProvider().supports(baseDir)).isTrue(); 175 | } 176 | 177 | private static JGitBlameCommand mockCommand() { 178 | return mock(JGitBlameCommand.class); 179 | } 180 | 181 | @Test 182 | public void branchChangedFiles_from_diverged() throws IOException, GitAPIException { 183 | createAndCommitFile("file-m1.xoo"); 184 | createAndCommitFile("file-m2.xoo"); 185 | createAndCommitFile("file-m3.xoo"); 186 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 187 | 188 | appendToAndCommitFile("file-m3.xoo"); 189 | createAndCommitFile("file-m4.xoo"); 190 | 191 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 192 | git.checkout().setName("b1").call(); 193 | createAndCommitFile("file-b1.xoo"); 194 | appendToAndCommitFile("file-m1.xoo"); 195 | deleteAndCommitFile("file-m2.xoo"); 196 | 197 | assertThat(newScmProvider().branchChangedFiles("master", worktree)) 198 | .containsExactlyInAnyOrder( 199 | worktree.resolve("file-b1.xoo"), 200 | worktree.resolve("file-m1.xoo")); 201 | } 202 | 203 | @Test 204 | public void branchChangedFiles_should_not_fail_with_patience_diff_algo() throws IOException { 205 | Path gitConfig = worktree.resolve(".git").resolve("config"); 206 | Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8)); 207 | Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); 208 | git = new Git(repo); 209 | 210 | assertThat(newScmProvider().branchChangedFiles("master", worktree)).isNull(); 211 | } 212 | 213 | @Test 214 | public void branchChangedFiles_from_merged_and_diverged() throws IOException, GitAPIException { 215 | createAndCommitFile("file-m1.xoo"); 216 | createAndCommitFile("file-m2.xoo"); 217 | createAndCommitFile("lao.txt", CONTENT_LAO); 218 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 219 | 220 | createAndCommitFile("file-m3.xoo"); 221 | ObjectId mergePoint = git.getRepository().exactRef("HEAD").getObjectId(); 222 | 223 | appendToAndCommitFile("file-m3.xoo"); 224 | createAndCommitFile("file-m4.xoo"); 225 | 226 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 227 | git.checkout().setName("b1").call(); 228 | createAndCommitFile("file-b1.xoo"); 229 | appendToAndCommitFile("file-m1.xoo"); 230 | deleteAndCommitFile("file-m2.xoo"); 231 | 232 | git.merge().include(mergePoint).call(); 233 | createAndCommitFile("file-b2.xoo"); 234 | 235 | createAndCommitFile("file-m5.xoo"); 236 | deleteAndCommitFile("file-m5.xoo"); 237 | 238 | Set changedFiles = newScmProvider().branchChangedFiles("master", worktree); 239 | assertThat(changedFiles) 240 | .containsExactlyInAnyOrder( 241 | worktree.resolve("file-m1.xoo"), 242 | worktree.resolve("file-b1.xoo"), 243 | worktree.resolve("file-b2.xoo")); 244 | 245 | // use a subset of changed files for .branchChangedLines to verify only requested files are returned 246 | assertThat(changedFiles.remove(worktree.resolve("file-b1.xoo"))).isTrue(); 247 | 248 | // generate common sample diff 249 | createAndCommitFile("lao.txt", CONTENT_TZU); 250 | changedFiles.add(worktree.resolve("lao.txt")); 251 | 252 | // a file that should not yield any results 253 | changedFiles.add(worktree.resolve("nonexistent")); 254 | 255 | assertThat(newScmProvider().branchChangedLines("master", worktree, changedFiles)) 256 | .isEqualTo( 257 | ImmutableMap.of( 258 | worktree.resolve("lao.txt"), ImmutableSet.of(2, 3, 11, 12, 13), 259 | worktree.resolve("file-m1.xoo"), ImmutableSet.of(4), 260 | worktree.resolve("file-b2.xoo"), ImmutableSet.of(1, 2, 3))); 261 | 262 | assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(worktree.resolve("nonexistent")))) 263 | .isEmpty(); 264 | } 265 | 266 | @Test 267 | public void forkDate_from_diverged() throws IOException, GitAPIException { 268 | createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS)); 269 | createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS)); 270 | Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS); 271 | createAndCommitFile("file-m3.xoo", expectedForkDate); 272 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 273 | 274 | appendToAndCommitFile("file-m3.xoo"); 275 | createAndCommitFile("file-m4.xoo"); 276 | 277 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 278 | git.checkout().setName("b1").call(); 279 | createAndCommitFile("file-b1.xoo"); 280 | appendToAndCommitFile("file-m1.xoo"); 281 | deleteAndCommitFile("file-m2.xoo"); 282 | 283 | assertThat(newScmProvider().forkDate("master", worktree)) 284 | .isEqualTo(expectedForkDate.truncatedTo(ChronoUnit.SECONDS)); 285 | } 286 | 287 | @Test 288 | public void forkDate_should_not_fail_if_reference_is_the_same_branch() throws IOException, GitAPIException { 289 | createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS)); 290 | createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS)); 291 | 292 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 293 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 294 | git.checkout().setName("b1").call(); 295 | 296 | Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS); 297 | createAndCommitFile("file-m3.xoo", expectedForkDate); 298 | 299 | assertThat(newScmProvider().forkDate("b1", worktree)) 300 | .isEqualTo(expectedForkDate.truncatedTo(ChronoUnit.SECONDS)); 301 | } 302 | 303 | @Test 304 | public void forkDate_should_not_fail_with_patience_diff_algo() throws IOException { 305 | Path gitConfig = worktree.resolve(".git").resolve("config"); 306 | Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8)); 307 | Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); 308 | git = new Git(repo); 309 | 310 | assertThat(newScmProvider().forkDate("master", worktree)).isNull(); 311 | } 312 | 313 | @Test 314 | public void forkDate_should_not_fail_with_invalid_basedir() throws IOException { 315 | assertThat(newScmProvider().forkDate("master", temp.newFolder().toPath())).isNull(); 316 | } 317 | 318 | @Test 319 | public void forkDate_should_not_fail_when_no_merge_base_is_found() throws IOException, GitAPIException { 320 | createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS)); 321 | 322 | git.checkout().setOrphan(true).setName("b1").call(); 323 | createAndCommitFile("file-b1.xoo"); 324 | 325 | assertThat(newScmProvider().forkDate("master", worktree)).isNull(); 326 | } 327 | 328 | @Test 329 | public void forkDate_without_target_branch() throws IOException, GitAPIException { 330 | createAndCommitFile("file-m1.xoo", Instant.now().minus(8, ChronoUnit.DAYS)); 331 | createAndCommitFile("file-m2.xoo", Instant.now().minus(7, ChronoUnit.DAYS)); 332 | Instant expectedForkDate = Instant.now().minus(6, ChronoUnit.DAYS); 333 | createAndCommitFile("file-m3.xoo", expectedForkDate); 334 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 335 | 336 | appendToAndCommitFile("file-m3.xoo"); 337 | createAndCommitFile("file-m4.xoo"); 338 | 339 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 340 | git.checkout().setName("b1").call(); 341 | createAndCommitFile("file-b1.xoo"); 342 | appendToAndCommitFile("file-m1.xoo"); 343 | deleteAndCommitFile("file-m2.xoo"); 344 | 345 | assertThat(newScmProvider().forkDate("unknown", worktree)).isNull(); 346 | } 347 | 348 | @Test 349 | public void branchChangedLines_should_be_correct_when_change_is_not_committed() throws GitAPIException, IOException { 350 | String fileName = "file-in-first-commit.xoo"; 351 | git.branchCreate().setName("b1").call(); 352 | git.checkout().setName("b1").call(); 353 | 354 | // this line is committed 355 | addLineToFile(fileName, 3); 356 | commit(fileName); 357 | 358 | // this line is not committed 359 | addLineToFile(fileName, 1); 360 | 361 | Path filePath = worktree.resolve(fileName); 362 | Map> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath)); 363 | 364 | // both lines appear correctly 365 | assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4)))); 366 | } 367 | 368 | @Test 369 | public void branchChangedLines_should_not_fail_if_there_is_no_merge_base() throws GitAPIException, IOException { 370 | createAndCommitFile("file-m1.xoo"); 371 | git.checkout().setOrphan(true).setName("b1").call(); 372 | createAndCommitFile("file-b1.xoo"); 373 | 374 | Map> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(Paths.get(""))); 375 | assertThat(changedLines).isNull(); 376 | } 377 | 378 | @Test 379 | public void branchChangedLines_returns_empty_set_for_files_with_lines_removed_only() throws GitAPIException, IOException { 380 | String fileName = "file-in-first-commit.xoo"; 381 | git.branchCreate().setName("b1").call(); 382 | git.checkout().setName("b1").call(); 383 | 384 | removeLineInFile(fileName, 2); 385 | commit(fileName); 386 | 387 | Path filePath = worktree.resolve(fileName); 388 | Map> changedLines = newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath)); 389 | 390 | // both lines appear correctly 391 | assertThat(changedLines).containsExactly(entry(filePath, emptySet())); 392 | } 393 | 394 | @Test 395 | public void branchChangedLines_uses_relative_paths_from_project_root() throws GitAPIException, IOException { 396 | String fileName = "project1/file-in-first-commit.xoo"; 397 | createAndCommitFile(fileName); 398 | 399 | git.branchCreate().setName("b1").call(); 400 | git.checkout().setName("b1").call(); 401 | 402 | // this line is committed 403 | addLineToFile(fileName, 3); 404 | commit(fileName); 405 | 406 | // this line is not committed 407 | addLineToFile(fileName, 1); 408 | 409 | Path filePath = worktree.resolve(fileName); 410 | Map> changedLines = newScmProvider().branchChangedLines("master", 411 | worktree.resolve("project1"), Collections.singleton(filePath)); 412 | 413 | // both lines appear correctly 414 | assertThat(changedLines).containsExactly(entry(filePath, new HashSet<>(Arrays.asList(1, 4)))); 415 | } 416 | 417 | @Test 418 | public void branchChangedFiles_when_git_work_tree_is_above_project_basedir() throws IOException, GitAPIException { 419 | git.branchCreate().setName("b1").call(); 420 | git.checkout().setName("b1").call(); 421 | 422 | Path projectDir = worktree.resolve("project"); 423 | Files.createDirectory(projectDir); 424 | createAndCommitFile("project/file-b1"); 425 | assertThat(newScmProvider().branchChangedFiles("master", projectDir)) 426 | .containsOnly(projectDir.resolve("file-b1")); 427 | } 428 | 429 | @Test 430 | public void branchChangedLines_should_not_fail_with_patience_diff_algo() throws IOException { 431 | Path gitConfig = worktree.resolve(".git").resolve("config"); 432 | Files.write(gitConfig, "[diff]\nalgorithm = patience\n".getBytes(StandardCharsets.UTF_8)); 433 | Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); 434 | git = new Git(repo); 435 | 436 | assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(Paths.get("file")))).isNull(); 437 | } 438 | 439 | /** 440 | * Unfortunately it looks like JGit doesn't support this setting using .gitattributes. 441 | */ 442 | @Test 443 | public void branchChangedLines_should_always_ignore_different_line_endings() throws IOException, GitAPIException { 444 | Path filePath = worktree.resolve("file-m1.xoo"); 445 | 446 | createAndCommitFile("file-m1.xoo"); 447 | ObjectId forkPoint = git.getRepository().exactRef("HEAD").getObjectId(); 448 | 449 | git.branchCreate().setName("b1").setStartPoint(forkPoint.getName()).call(); 450 | git.checkout().setName("b1").call(); 451 | 452 | String newFileContent = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8).replaceAll("\n", "\r\n"); 453 | Files.write(filePath, newFileContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING); 454 | commit("file-m1.xoo"); 455 | 456 | assertThat(newScmProvider().branchChangedLines("master", worktree, Collections.singleton(filePath))) 457 | .isEmpty(); 458 | } 459 | 460 | @Test 461 | public void branchChangedFiles_falls_back_to_origin_when_local_branch_does_not_exist() throws IOException, GitAPIException { 462 | git.branchCreate().setName("b1").call(); 463 | git.checkout().setName("b1").call(); 464 | createAndCommitFile("file-b1"); 465 | 466 | Path worktree2 = temp.newFolder().toPath(); 467 | Git.cloneRepository() 468 | .setURI(worktree.toString()) 469 | .setDirectory(worktree2.toFile()) 470 | .call(); 471 | 472 | assertThat(newScmProvider().branchChangedFiles("master", worktree2)) 473 | .containsOnly(worktree2.resolve("file-b1")); 474 | verifyZeroInteractions(analysisWarnings); 475 | } 476 | 477 | @Test 478 | public void branchChangedFiles_use_remote_target_ref_when_running_on_circle_ci() throws IOException, GitAPIException { 479 | when(system2.envVariable("CIRCLECI")).thenReturn("true"); 480 | git.checkout().setName("b1").setCreateBranch(true).call(); 481 | createAndCommitFile("file-b1"); 482 | 483 | Path worktree2 = temp.newFolder().toPath(); 484 | Git local = Git.cloneRepository() 485 | .setURI(worktree.toString()) 486 | .setDirectory(worktree2.toFile()) 487 | .call(); 488 | 489 | // Make local master match analyzed branch, so if local ref is used then change files will be empty 490 | local.checkout().setCreateBranch(true).setName("master").setStartPoint("origin/b1").call(); 491 | local.checkout().setName("b1").call(); 492 | 493 | assertThat(newScmProvider().branchChangedFiles("master", worktree2)) 494 | .containsOnly(worktree2.resolve("file-b1")); 495 | verifyZeroInteractions(analysisWarnings); 496 | } 497 | 498 | @Test 499 | public void branchChangedFiles_falls_back_to_local_ref_if_origin_branch_does_not_exist_when_running_on_circle_ci() throws IOException, GitAPIException { 500 | when(system2.envVariable("CIRCLECI")).thenReturn("true"); 501 | git.checkout().setName("b1").setCreateBranch(true).call(); 502 | createAndCommitFile("file-b1"); 503 | 504 | Path worktree2 = temp.newFolder().toPath(); 505 | Git local = Git.cloneRepository() 506 | .setURI(worktree.toString()) 507 | .setDirectory(worktree2.toFile()) 508 | .call(); 509 | 510 | local.checkout().setName("local-only").setCreateBranch(true).setStartPoint("origin/master").call(); 511 | local.checkout().setName("b1").call(); 512 | 513 | assertThat(newScmProvider().branchChangedFiles("local-only", worktree2)) 514 | .containsOnly(worktree2.resolve("file-b1")); 515 | verifyZeroInteractions(analysisWarnings); 516 | } 517 | 518 | @Test 519 | public void branchChangedFiles_falls_back_to_upstream_ref() throws IOException, GitAPIException { 520 | git.branchCreate().setName("b1").call(); 521 | git.checkout().setName("b1").call(); 522 | createAndCommitFile("file-b1"); 523 | 524 | Path worktree2 = temp.newFolder().toPath(); 525 | Git.cloneRepository() 526 | .setURI(worktree.toString()) 527 | .setRemote("upstream") 528 | .setDirectory(worktree2.toFile()) 529 | .call(); 530 | 531 | assertThat(newScmProvider().branchChangedFiles("master", worktree2)) 532 | .containsOnly(worktree2.resolve("file-b1")); 533 | verifyZeroInteractions(analysisWarnings); 534 | 535 | } 536 | 537 | @Test 538 | public void branchChangedFiles_should_return_null_when_branch_nonexistent() { 539 | assertThat(newScmProvider().branchChangedFiles("nonexistent", worktree)).isNull(); 540 | } 541 | 542 | @Test 543 | public void branchChangedFiles_should_throw_when_repo_nonexistent() throws IOException { 544 | thrown.expect(MessageException.class); 545 | thrown.expectMessage("Not inside a Git work tree: "); 546 | newScmProvider().branchChangedFiles("master", temp.newFolder().toPath()); 547 | } 548 | 549 | @Test 550 | public void branchChangedFiles_should_throw_when_dir_nonexistent() { 551 | thrown.expect(MessageException.class); 552 | thrown.expectMessage("Not inside a Git work tree: "); 553 | newScmProvider().branchChangedFiles("master", temp.getRoot().toPath().resolve("nonexistent")); 554 | } 555 | 556 | @Test 557 | public void branchChangedFiles_should_return_null_on_io_errors_of_repo_builder() { 558 | GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { 559 | @Override 560 | Repository buildRepo(Path basedir) throws IOException { 561 | throw new IOException(); 562 | } 563 | }; 564 | assertThat(provider.branchChangedFiles("branch", worktree)).isNull(); 565 | verifyZeroInteractions(analysisWarnings); 566 | } 567 | 568 | @Test 569 | public void branchChangedFiles_should_return_null_if_repo_exactref_is_null() throws IOException { 570 | Repository repository = mock(Repository.class); 571 | RefDatabase refDatabase = mock(RefDatabase.class); 572 | when(repository.getRefDatabase()).thenReturn(refDatabase); 573 | when(refDatabase.findRef("branch")).thenReturn(null); 574 | 575 | GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { 576 | @Override 577 | Repository buildRepo(Path basedir) { 578 | return repository; 579 | } 580 | }; 581 | assertThat(provider.branchChangedFiles("branch", worktree)).isNull(); 582 | 583 | String warning = "Could not find ref 'branch' in refs/heads, refs/remotes/upstream or refs/remotes/origin." 584 | + " You may see unexpected issues and changes. Please make sure to fetch this ref before pull request analysis."; 585 | verify(analysisWarnings).addUnique(warning); 586 | } 587 | 588 | @Test 589 | public void branchChangedFiles_should_return_null_on_errors() throws GitAPIException { 590 | DiffCommand diffCommand = mock(DiffCommand.class); 591 | when(diffCommand.setShowNameAndStatusOnly(anyBoolean())).thenReturn(diffCommand); 592 | when(diffCommand.setOldTree(any())).thenReturn(diffCommand); 593 | when(diffCommand.setNewTree(any())).thenReturn(diffCommand); 594 | when(diffCommand.call()).thenThrow(mock(GitAPIException.class)); 595 | 596 | Git git = mock(Git.class); 597 | when(git.diff()).thenReturn(diffCommand); 598 | 599 | GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { 600 | @Override 601 | Git newGit(Repository repo) { 602 | return git; 603 | } 604 | }; 605 | assertThat(provider.branchChangedFiles("master", worktree)).isNull(); 606 | verify(diffCommand).call(); 607 | } 608 | 609 | @Test 610 | public void branchChangedLines_returns_null_when_branch_doesnt_exist() { 611 | assertThat(newScmProvider().branchChangedLines("nonexistent", worktree, emptySet())).isNull(); 612 | } 613 | 614 | @Test 615 | public void branchChangedLines_omits_files_with_git_api_errors() throws IOException, GitAPIException { 616 | String f1 = "file-in-first-commit.xoo"; 617 | String f2 = "file2-in-first-commit.xoo"; 618 | 619 | createAndCommitFile(f2); 620 | 621 | git.branchCreate().setName("b1").call(); 622 | git.checkout().setName("b1").call(); 623 | 624 | // both files modified 625 | addLineToFile(f1, 1); 626 | addLineToFile(f2, 2); 627 | 628 | commit(f1); 629 | commit(f2); 630 | 631 | AtomicInteger callCount = new AtomicInteger(0); 632 | GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { 633 | @Override 634 | AbstractTreeIterator prepareTreeParser(Repository repo, RevCommit commit) throws IOException { 635 | if (callCount.getAndIncrement() == 1) { 636 | throw new RuntimeException("error"); 637 | } 638 | return super.prepareTreeParser(repo, commit); 639 | } 640 | }; 641 | Set changedFiles = new LinkedHashSet<>(); 642 | changedFiles.add(worktree.resolve(f1)); 643 | changedFiles.add(worktree.resolve(f2)); 644 | 645 | assertThat(provider.branchChangedLines("master", worktree, changedFiles)) 646 | .isEqualTo(Collections.singletonMap(worktree.resolve(f1), Collections.singleton(1))); 647 | } 648 | 649 | @Test 650 | public void branchChangedLines_returns_null_on_io_errors_of_repo_builder() { 651 | GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) { 652 | @Override 653 | Repository buildRepo(Path basedir) throws IOException { 654 | throw new IOException(); 655 | } 656 | }; 657 | assertThat(provider.branchChangedLines("branch", worktree, emptySet())).isNull(); 658 | } 659 | 660 | @Test 661 | public void relativePathFromScmRoot_should_return_dot_project_root() { 662 | assertThat(newGitScmProvider().relativePathFromScmRoot(worktree)).isEqualTo(Paths.get("")); 663 | } 664 | 665 | private GitScmProvider newGitScmProvider() { 666 | return new GitScmProvider(mock(JGitBlameCommand.class), analysisWarnings, gitIgnoreCommand, system2); 667 | } 668 | 669 | @Test 670 | public void relativePathFromScmRoot_should_return_filename_for_file_in_project_root() throws IOException { 671 | Path filename = Paths.get("somefile.xoo"); 672 | Path path = worktree.resolve(filename); 673 | Files.createFile(path); 674 | assertThat(newGitScmProvider().relativePathFromScmRoot(path)).isEqualTo(filename); 675 | } 676 | 677 | @Test 678 | public void relativePathFromScmRoot_should_return_relative_path_for_file_in_project_subdir() throws IOException { 679 | Path relpath = Paths.get("sub/dir/to/somefile.xoo"); 680 | Path path = worktree.resolve(relpath); 681 | Files.createDirectories(path.getParent()); 682 | Files.createFile(path); 683 | assertThat(newGitScmProvider().relativePathFromScmRoot(path)).isEqualTo(relpath); 684 | } 685 | 686 | @Test 687 | public void revisionId_should_return_different_sha1_after_commit() throws IOException, GitAPIException { 688 | Path projectDir = worktree.resolve("project"); 689 | Files.createDirectory(projectDir); 690 | 691 | GitScmProvider provider = newGitScmProvider(); 692 | 693 | String sha1before = provider.revisionId(projectDir); 694 | assertThat(sha1before).hasSize(40); 695 | 696 | createAndCommitFile("project/file1"); 697 | String sha1after = provider.revisionId(projectDir); 698 | assertThat(sha1after).hasSize(40); 699 | 700 | assertThat(sha1after).isNotEqualTo(sha1before); 701 | assertThat(provider.revisionId(projectDir)).isEqualTo(sha1after); 702 | } 703 | 704 | @Test 705 | public void revisionId_should_return_null_in_empty_repo() throws IOException { 706 | worktree = temp.newFolder().toPath(); 707 | Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); 708 | repo.create(); 709 | 710 | git = new Git(repo); 711 | 712 | Path projectDir = worktree.resolve("project"); 713 | Files.createDirectory(projectDir); 714 | 715 | GitScmProvider provider = newGitScmProvider(); 716 | 717 | assertThat(provider.revisionId(projectDir)).isNull(); 718 | } 719 | 720 | private String randomizedContent(String prefix, int numLines) { 721 | StringBuilder sb = new StringBuilder(); 722 | for (int line = 0; line < numLines; line++) { 723 | sb.append(randomizedLine(prefix)); 724 | sb.append("\n"); 725 | } 726 | return sb.toString(); 727 | } 728 | 729 | private String randomizedLine(String prefix) { 730 | StringBuilder sb = new StringBuilder(prefix); 731 | for (int i = 0; i < 4; i++) { 732 | sb.append(' '); 733 | for (int j = 0; j < prefix.length(); j++) { 734 | sb.append((char) ('a' + random.nextInt(26))); 735 | } 736 | } 737 | return sb.toString(); 738 | } 739 | 740 | private void createAndCommitFile(String relativePath) throws IOException, GitAPIException { 741 | createAndCommitFile(relativePath, randomizedContent(relativePath, 3)); 742 | } 743 | 744 | private void createAndCommitFile(String relativePath, Instant commitDate) throws IOException, GitAPIException { 745 | createFile(relativePath, randomizedContent(relativePath, 3)); 746 | commit(relativePath, commitDate); 747 | } 748 | 749 | private void createAndCommitFile(String relativePath, String content) throws IOException, GitAPIException { 750 | createFile(relativePath, content); 751 | commit(relativePath); 752 | } 753 | 754 | private void createFile(String relativePath, String content) throws IOException { 755 | Path newFile = worktree.resolve(relativePath); 756 | Files.createDirectories(newFile.getParent()); 757 | Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE); 758 | } 759 | 760 | private void addLineToFile(String relativePath, int lineNumber) throws IOException { 761 | Path filePath = worktree.resolve(relativePath); 762 | List lines = Files.readAllLines(filePath); 763 | lines.add(lineNumber - 1, randomizedLine(relativePath)); 764 | Files.write(filePath, lines, StandardOpenOption.TRUNCATE_EXISTING); 765 | } 766 | 767 | private void removeLineInFile(String relativePath, int lineNumber) throws IOException { 768 | Path filePath = worktree.resolve(relativePath); 769 | List lines = Files.readAllLines(filePath); 770 | lines.remove(lineNumber - 1); 771 | Files.write(filePath, lines, StandardOpenOption.TRUNCATE_EXISTING); 772 | } 773 | 774 | private void appendToAndCommitFile(String relativePath) throws IOException, GitAPIException { 775 | Files.write(worktree.resolve(relativePath), randomizedContent(relativePath, 1).getBytes(), StandardOpenOption.APPEND); 776 | commit(relativePath); 777 | } 778 | 779 | private void deleteAndCommitFile(String relativePath) throws GitAPIException { 780 | git.rm().addFilepattern(relativePath).call(); 781 | commit(relativePath); 782 | } 783 | 784 | private void commit(String... relativePaths) throws GitAPIException { 785 | for (String path : relativePaths) { 786 | git.add().addFilepattern(path).call(); 787 | } 788 | String msg = String.join(",", relativePaths); 789 | git.commit().setAuthor("joe", "joe@example.com").setMessage(msg).call(); 790 | } 791 | 792 | private void commit(String relativePath, Instant date) throws GitAPIException { 793 | PersonIdent person = new PersonIdent("joe", "joe@example.com", Date.from(date), TimeZone.getDefault()); 794 | git.commit().setAuthor(person).setCommitter(person).setMessage(relativePath).call(); 795 | } 796 | 797 | private GitScmProvider newScmProvider() { 798 | return new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2); 799 | } 800 | } 801 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/GitThreadFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.util.concurrent.ForkJoinPool; 23 | import org.junit.Test; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | public class GitThreadFactoryTest { 28 | @Test 29 | public void testName() { 30 | GitThreadFactory factory = new GitThreadFactory(); 31 | ForkJoinPool pool = new ForkJoinPool(); 32 | assertThat(factory.newThread(pool).getName()).isEqualTo("git-scm-0"); 33 | assertThat(factory.newThread(pool).getName()).isEqualTo("git-scm-1"); 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/JGitBlameCommandTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 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.util.Arrays; 27 | import java.util.Collections; 28 | import java.util.Date; 29 | import java.util.LinkedHashMap; 30 | import java.util.LinkedList; 31 | import java.util.List; 32 | import java.util.Map; 33 | import org.apache.commons.io.FileUtils; 34 | import org.junit.Rule; 35 | import org.junit.Test; 36 | import org.junit.rules.ExpectedException; 37 | import org.junit.rules.TemporaryFolder; 38 | import org.sonar.api.batch.fs.InputFile; 39 | import org.sonar.api.batch.fs.internal.DefaultFileSystem; 40 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 41 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 42 | import org.sonar.api.batch.scm.BlameCommand.BlameInput; 43 | import org.sonar.api.batch.scm.BlameCommand.BlameOutput; 44 | import org.sonar.api.batch.scm.BlameLine; 45 | import org.sonar.api.notifications.AnalysisWarnings; 46 | import org.sonar.api.scan.filesystem.PathResolver; 47 | import org.sonar.api.utils.DateUtils; 48 | import org.sonar.api.utils.MessageException; 49 | import org.sonar.api.utils.System2; 50 | import org.sonar.api.utils.log.LogTester; 51 | 52 | import static org.assertj.core.api.Assertions.assertThat; 53 | import static org.junit.Assume.assumeTrue; 54 | import static org.mockito.Matchers.startsWith; 55 | import static org.mockito.Mockito.mock; 56 | import static org.mockito.Mockito.verify; 57 | import static org.mockito.Mockito.verifyZeroInteractions; 58 | import static org.mockito.Mockito.when; 59 | import static org.sonarsource.scm.git.Utils.javaUnzip; 60 | 61 | public class JGitBlameCommandTest { 62 | 63 | private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java"; 64 | 65 | @Rule 66 | public ExpectedException thrown = ExpectedException.none(); 67 | 68 | @Rule 69 | public TemporaryFolder temp = new TemporaryFolder(); 70 | 71 | @Rule 72 | public LogTester logTester = new LogTester(); 73 | 74 | private final BlameInput input = mock(BlameInput.class); 75 | 76 | @Test 77 | public void testBlame() throws IOException { 78 | File projectDir = temp.newFolder(); 79 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 80 | 81 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 82 | 83 | File baseDir = new File(projectDir, "dummy-git"); 84 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 85 | when(input.fileSystem()).thenReturn(fs); 86 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) 87 | .setModuleBaseDir(baseDir.toPath()) 88 | .build(); 89 | fs.add(inputFile); 90 | 91 | BlameOutput blameResult = mock(BlameOutput.class); 92 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 93 | jGitBlameCommand.blame(input, blameResult); 94 | 95 | Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200"); 96 | String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea"; 97 | String author1 = "david@gageot.net"; 98 | 99 | // second commit, which has a commit date different than the author date 100 | Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200"); 101 | String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7"; 102 | String author2 = "duarte.meneses@sonarsource.com"; 103 | 104 | List expectedBlame = new LinkedList<>(); 105 | for (int i = 0; i < 25; i++) { 106 | expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1)); 107 | } 108 | for (int i = 0; i < 3; i++) { 109 | expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2)); 110 | } 111 | for (int i = 0; i < 1; i++) { 112 | expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1)); 113 | } 114 | 115 | verify(blameResult).blameResult(inputFile, expectedBlame); 116 | } 117 | 118 | @Test 119 | public void properFailureIfNotAGitProject() throws IOException { 120 | File projectDir = temp.newFolder(); 121 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 122 | 123 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 124 | 125 | File baseDir = new File(projectDir, "dummy-git"); 126 | 127 | // Delete .git 128 | FileUtils.forceDelete(new File(baseDir, ".git")); 129 | 130 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 131 | when(input.fileSystem()).thenReturn(fs); 132 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).build(); 133 | fs.add(inputFile); 134 | 135 | BlameOutput blameResult = mock(BlameOutput.class); 136 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 137 | 138 | thrown.expect(MessageException.class); 139 | thrown.expectMessage("Not inside a Git work tree: "); 140 | 141 | jGitBlameCommand.blame(input, blameResult); 142 | } 143 | 144 | @Test 145 | public void testBlameOnNestedModule() throws IOException { 146 | File projectDir = temp.newFolder(); 147 | javaUnzip(new File("test-repos/dummy-git-nested.zip"), projectDir); 148 | 149 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 150 | 151 | File baseDir = new File(projectDir, "dummy-git-nested/dummy-project"); 152 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 153 | when(input.fileSystem()).thenReturn(fs); 154 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) 155 | .setModuleBaseDir(baseDir.toPath()) 156 | .build(); 157 | fs.add(inputFile); 158 | 159 | BlameOutput blameResult = mock(BlameOutput.class); 160 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 161 | jGitBlameCommand.blame(input, blameResult); 162 | 163 | Date revisionDate = DateUtils.parseDateTime("2012-07-17T16:12:48+0200"); 164 | String revision = "6b3aab35a3ea32c1636fee56f996e677653c48ea"; 165 | String author = "david@gageot.net"; 166 | verify(blameResult).blameResult(inputFile, 167 | Arrays.asList( 168 | new BlameLine().revision(revision).date(revisionDate).author(author), 169 | new BlameLine().revision(revision).date(revisionDate).author(author), 170 | new BlameLine().revision(revision).date(revisionDate).author(author), 171 | new BlameLine().revision(revision).date(revisionDate).author(author), 172 | new BlameLine().revision(revision).date(revisionDate).author(author), 173 | new BlameLine().revision(revision).date(revisionDate).author(author), 174 | new BlameLine().revision(revision).date(revisionDate).author(author), 175 | new BlameLine().revision(revision).date(revisionDate).author(author), 176 | new BlameLine().revision(revision).date(revisionDate).author(author), 177 | new BlameLine().revision(revision).date(revisionDate).author(author), 178 | new BlameLine().revision(revision).date(revisionDate).author(author), 179 | new BlameLine().revision(revision).date(revisionDate).author(author), 180 | new BlameLine().revision(revision).date(revisionDate).author(author), 181 | new BlameLine().revision(revision).date(revisionDate).author(author), 182 | new BlameLine().revision(revision).date(revisionDate).author(author), 183 | new BlameLine().revision(revision).date(revisionDate).author(author), 184 | new BlameLine().revision(revision).date(revisionDate).author(author), 185 | new BlameLine().revision(revision).date(revisionDate).author(author), 186 | new BlameLine().revision(revision).date(revisionDate).author(author), 187 | new BlameLine().revision(revision).date(revisionDate).author(author), 188 | new BlameLine().revision(revision).date(revisionDate).author(author), 189 | new BlameLine().revision(revision).date(revisionDate).author(author), 190 | new BlameLine().revision(revision).date(revisionDate).author(author), 191 | new BlameLine().revision(revision).date(revisionDate).author(author), 192 | new BlameLine().revision(revision).date(revisionDate).author(author), 193 | new BlameLine().revision(revision).date(revisionDate).author(author))); 194 | } 195 | 196 | @Test 197 | public void dontFailOnModifiedFile() throws IOException { 198 | File projectDir = temp.newFolder(); 199 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 200 | 201 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 202 | 203 | File baseDir = new File(projectDir, "dummy-git"); 204 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 205 | when(input.fileSystem()).thenReturn(fs); 206 | String relativePath = DUMMY_JAVA; 207 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath).build(); 208 | fs.add(inputFile); 209 | 210 | // Emulate a modification 211 | Files.write(baseDir.toPath().resolve(relativePath), "modification and \n some new line".getBytes()); 212 | 213 | BlameOutput blameResult = mock(BlameOutput.class); 214 | 215 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); 216 | jGitBlameCommand.blame(input, blameResult); 217 | } 218 | 219 | @Test 220 | public void dontFailOnNewFile() throws IOException { 221 | File projectDir = temp.newFolder(); 222 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 223 | 224 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 225 | 226 | File baseDir = new File(projectDir, "dummy-git"); 227 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 228 | when(input.fileSystem()).thenReturn(fs); 229 | String relativePath = DUMMY_JAVA; 230 | String relativePath2 = "src/main/java/org/dummy/Dummy2.java"; 231 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath).build(); 232 | fs.add(inputFile); 233 | DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", relativePath2).build(); 234 | fs.add(inputFile2); 235 | 236 | // Emulate a new file 237 | FileUtils.copyFile(new File(baseDir, relativePath), new File(baseDir, relativePath2)); 238 | 239 | BlameOutput blameResult = mock(BlameOutput.class); 240 | 241 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2)); 242 | jGitBlameCommand.blame(input, blameResult); 243 | } 244 | 245 | @Test 246 | public void dontFailOnSymlink() throws IOException { 247 | assumeTrue(!System2.INSTANCE.isOsWindows()); 248 | File projectDir = temp.newFolder(); 249 | javaUnzip(new File("test-repos/dummy-git.zip"), projectDir); 250 | 251 | JGitBlameCommand jGitBlameCommand = newJGitBlameCommand(); 252 | 253 | File baseDir = new File(projectDir, "dummy-git"); 254 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 255 | when(input.fileSystem()).thenReturn(fs); 256 | String relativePath = DUMMY_JAVA; 257 | String relativePath2 = "src/main/java/org/dummy/Dummy2.java"; 258 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath) 259 | .setModuleBaseDir(baseDir.toPath()) 260 | .build(); 261 | fs.add(inputFile); 262 | DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", relativePath2) 263 | .setModuleBaseDir(baseDir.toPath()) 264 | .build(); 265 | fs.add(inputFile2); 266 | 267 | // Create symlink 268 | Files.createSymbolicLink(inputFile2.file().toPath(), inputFile.file().toPath()); 269 | 270 | BlameOutput blameResult = mock(BlameOutput.class); 271 | 272 | when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2)); 273 | jGitBlameCommand.blame(input, blameResult); 274 | } 275 | 276 | @Test 277 | public void return_early_when_shallow_clone_detected() throws IOException { 278 | File projectDir = temp.newFolder(); 279 | javaUnzip(new File("test-repos/shallow-git.zip"), projectDir); 280 | 281 | File baseDir = new File(projectDir, "shallow-git"); 282 | 283 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 284 | when(input.fileSystem()).thenReturn(fs); 285 | 286 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).build(); 287 | when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile)); 288 | 289 | // register warning with default wrapper 290 | AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); 291 | JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings); 292 | BlameOutput output = mock(BlameOutput.class); 293 | jGitBlameCommand.blame(input, output); 294 | 295 | assertThat(logTester.logs()).first() 296 | .matches(s -> s.contains("Shallow clone detected, no blame information will be provided.")); 297 | verifyZeroInteractions(output); 298 | 299 | verify(analysisWarnings).addUnique(startsWith("Shallow clone detected")); 300 | } 301 | 302 | @Test 303 | public void return_early_when_clone_with_reference_detected() throws IOException { 304 | File projectDir = temp.newFolder(); 305 | javaUnzip(new File("test-repos/dummy-git-reference-clone.zip"), projectDir); 306 | 307 | Path baseDir = projectDir.toPath().resolve("dummy-git2"); 308 | 309 | DefaultFileSystem fs = new DefaultFileSystem(baseDir); 310 | when(input.fileSystem()).thenReturn(fs); 311 | 312 | DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).setModuleBaseDir(baseDir).build(); 313 | when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile)); 314 | 315 | // register warning 316 | AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); 317 | JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings); 318 | TestBlameOutput output = new TestBlameOutput(); 319 | jGitBlameCommand.blame(input, output); 320 | 321 | assertThat(logTester.logs()).first() 322 | .matches(s -> s.contains("This git repository references another local repository which is not well supported")); 323 | 324 | // contains commits referenced from the old clone and commits in the new clone 325 | assertThat(output.blame.keySet()).contains(inputFile); 326 | assertThat(output.blame.get(inputFile).stream().map(BlameLine::revision)) 327 | .containsOnly("6b3aab35a3ea32c1636fee56f996e677653c48ea", "843c7c30d7ebd9a479e8f1daead91036c75cbc4e", "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7"); 328 | verifyZeroInteractions(analysisWarnings); 329 | } 330 | 331 | private JGitBlameCommand newJGitBlameCommand() { 332 | return new JGitBlameCommand(new PathResolver(), mock(AnalysisWarnings.class)); 333 | } 334 | 335 | private static class TestBlameOutput implements BlameOutput { 336 | private Map> blame = new LinkedHashMap<>(); 337 | 338 | @Override public void blameResult(InputFile inputFile, List list) { 339 | blame.put(inputFile, list); 340 | } 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/src/test/java/org/sonarsource/scm/git/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: Plugins :: SCM :: Git 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.sonarsource.scm.git; 21 | 22 | import java.io.File; 23 | import java.nio.file.Files; 24 | import java.util.Enumeration; 25 | import java.util.zip.ZipEntry; 26 | import java.util.zip.ZipFile; 27 | import org.apache.commons.io.FileUtils; 28 | 29 | import static java.lang.String.format; 30 | 31 | public class Utils { 32 | 33 | public static void javaUnzip(File zip, File toDir) { 34 | try { 35 | try (ZipFile zipFile = new ZipFile(zip)) { 36 | Enumeration entries = zipFile.entries(); 37 | while (entries.hasMoreElements()) { 38 | ZipEntry entry = entries.nextElement(); 39 | File to = new File(toDir, entry.getName()); 40 | if (entry.isDirectory()) { 41 | FileUtils.forceMkdir(to); 42 | } else { 43 | File parent = to.getParentFile(); 44 | if (parent != null) { 45 | FileUtils.forceMkdir(parent); 46 | } 47 | 48 | Files.copy(zipFile.getInputStream(entry), to.toPath()); 49 | } 50 | } 51 | } 52 | } catch (Exception e) { 53 | throw new IllegalStateException(format("Fail to unzip %s to %s", zip, toDir), e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/dummy-git-nested.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/dummy-git-nested.zip -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/dummy-git-reference-clone.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/dummy-git-reference-clone.zip -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/dummy-git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/dummy-git.zip -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/ignore-git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/ignore-git.zip -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/reference-git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/reference-git.zip -------------------------------------------------------------------------------- /sonar-scm-git-plugin/test-repos/shallow-git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-scm-git/ab369886e2299877897cda3cc905f629736d6e5c/sonar-scm-git-plugin/test-repos/shallow-git.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 | --------------------------------------------------------------------------------