├── .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 | [](https://cirrus-ci.com/github/SonarSource/sonar-scm-git) [](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 extends ZipEntry> 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 |
--------------------------------------------------------------------------------