diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000000..036fd42ea4 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,11 @@ +version: 1 + +update_configs: + - package_manager: "java:maven" + directory: "/" + update_schedule: "weekly" + target_branch: "master" + default_reviewers: + - "MarkEWaite" + default_labels: + - "dependencies" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..8e1c676c11 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## [JENKINS-xxxxx](https://issues.jenkins-ci.org/browse/JENKINS-xxxxx) - summarize pull request in one line + +Describe the big picture of your changes here to explain to the maintainers why we should accept this pull request. +If it fixes a bug or resolves a feature request, include a link to the issue. + +## Checklist + +_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. This is simply a reminder of what we are going to look for before merging your code. If a checkbox or line does not apply to this pull request, delete it. We prefer all checkboxes to be checked before a pull request is merged_ + +- [ ] I have read the [CONTRIBUTING](https://github.com/jenkinsci/git-plugin/blob/master/CONTRIBUTING.adoc) doc +- [ ] I have referenced the Jira issue related to my changes in one or more commit messages +- [ ] I have added tests that verify my changes +- [ ] Unit tests pass locally with my changes +- [ ] I have added documentation as necessary +- [ ] No Javadoc warnings were introduced with my changes +- [ ] No spotbugs warnings were introduced with my changes +- [ ] I have interactively tested my changes +- [ ] Any dependent changes have been merged and published in upstream modules (like git-client-plugin) + +## Types of changes + +What types of changes does your code introduce? _Put an `x` in the boxes that apply. Delete the items in the list that do *not* apply_ + +- [ ] Dependency or infrastructure update +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Further comments + +If this is a relatively large or complex change, start the discussion by explaining why you chose the solution you did and what alternatives you considered. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000000..eda61e933c --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,3 @@ +_extends: .github +tag-template: git-$NEXT_PATCH_VERSION +version-template: $MAJOR.$MINOR.$PATCH diff --git a/.gitignore b/.gitignore index ca8263c864..7435cc90a2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,17 @@ bin *.iws work nbactions.xml +nb-configuration.xml release.properties pom.xml.releaseBackup .idea +*.sublime-project +*.sublime-workspace + +# vim +*.swp +Session.vim +/nbproject/ + +# Mac OSX +.DS_Store \ No newline at end of file diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 0000000000..94863e605b --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.0-beta-7 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000000..2a0299c486 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Pconsume-incrementals +-Pmight-produce-incrementals diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1 @@ + diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc new file mode 100644 index 0000000000..1fa884d2f9 --- /dev/null +++ b/CHANGELOG.adoc @@ -0,0 +1,1226 @@ +[[changelog-moved-to-github-releases]] += Changelog moved to https://github.com/jenkinsci/git-plugin/releases[GitHub Releases] + +== See https://github.com/jenkinsci/git-plugin/releases[GitHub Releases] for all new changelogs (July 1, 2019) + +== Version 3.10.0 (May 2, 2019) + +* Require Java 8 +* Require Jenkins 2.121.1 or newer +* Fix upgrade compatibility error for mergeStrategy 'default' of +pre-build merge in pipeline jobs +(https://issues.jenkins-ci.org/browse/JENKINS-51638[JENKINS-51638]) + +== Version 3.9.4 (April 24, 2019) + +* https://jenkins.io/security/advisory/2019-01-28/[Security advisory] Fix object not +found exception scanning multibranch pipeline +repo (https://issues.jenkins-ci.org/browse/JENKINS-50394[JENKINS-50394]) + +== Version 4.0.0-rc (January 30, 2019) + +* Require Java 8 +* Require Jenkins 2.60 +* Make matrix project dependency optional +* Add shallow cloning for submodules +(https://issues.jenkins-ci.org/browse/JENKINS-21248[JENKINS-21248]) +* Add option to search for users by e-mail address +(https://issues.jenkins-ci.org/browse/JENKINS-9016[JENKINS-9016]) +* Add parallel update for submodules +(https://issues.jenkins-ci.org/browse/JENKINS-44720[JENKINS-44720]) +* Stop bloating build.xml files with BuildData +(https://issues.jenkins-ci.org/browse/JENKINS-19022[JENKINS-19022]) +* Fix notifyCommit for branch names that contain '/' characters +(https://issues.jenkins-ci.org/browse/JENKINS-29603[JENKINS-29603], +https://issues.jenkins-ci.org/browse/JENKINS-32174[JENKINS-32174]) +* Fix empty "depth" parameter handling for shallow cloning +(https://issues.jenkins-ci.org/browse/JENKINS-53050[JENKINS-53050]) +* Ignore exceptions when generating commit message as informational +message in build log +(https://issues.jenkins-ci.org/browse/JENKINS-53725[JENKINS-53725]) +* Fix snippet generator gitlab version class cast exception +(https://issues.jenkins-ci.org/browse/JENKINS-46650[JENKINS-46650]) +* Fix git tool references on agent +(https://issues.jenkins-ci.org/browse/JENKINS-55827[JENKINS-55827]) + +== Version 3.9.3 (January 30, 2019) + +* https://jenkins.io/security/advisory/2019-01-28/[Fix local tool reference was ignored] +(https://issues.jenkins-ci.org/browse/JENKINS-55827[JENKINS-55827]), +regression in 3.9.2 + +== Version 3.9.2 (January 28, 2019) + +* https://jenkins.io/security/advisory/2019-01-28/[Fix security +issue] + +== Version 3.9.1 (June 4, 2018) + +* Fix security issue +(https://jenkins.io/security/advisory/2018-06-04/[security advisory]) + +== Version 3.9.0 (May 12, 2018) + +* Require Jenkins 1.642.3 instead of 1.625.3 (workflow dependency +update) +* Test automation improvements +(https://issues.jenkins-ci.org/browse/JENKINS-50621[JENKINS-50621], +https://issues.jenkins-ci.org/browse/JENKINS-50540[JENKINS-50540], +https://issues.jenkins-ci.org/browse/JENKINS-50777[JENKINS-50777]) +* Support SHA1 references in Pipeline shared libraries +(https://issues.jenkins-ci.org/browse/JENKINS-48061[JENKINS-48061]) +* Added a new trait enabling discovery of custom refs +(https://issues.jenkins-ci.org/browse/JENKINS-48061[JENKINS-48061]) +* Don't require a workspace for polling in Freestyle projects that +use ChangeLogToBranch extension +* Don't require a workspace for polling in Freestyle projects that +use author in changelog extension +(https://issues.jenkins-ci.org/browse/JENKINS-50683[JENKINS-50683] and +https://groups.google.com/d/msg/jenkinsci-dev/irft9lJIYVk/xnhNnrWcjJgJ[google groups discussion]) +* Correct the Pipeline data binding for merge strategy in +UserMergeOptions +(https://issues.jenkins-ci.org/browse/JENKINS-34070[JENKINS-34070]) +* Retain repository browser URL when saved from Pipeline job +definition page +(https://issues.jenkins-ci.org/browse/JENKINS-36451[JENKINS-36451]) + +== Version 3.8.0 (February 26, 2018) + +* Fix security issue +(https://jenkins.io/security/advisory/2018-02-26/[security advisory]) + +== Version 3.7.0 (December 21, 2017) + +* Fix checkout performance regression due to many rev-parse calls +(https://issues.jenkins-ci.org/browse/JENKINS-45447[JENKINS-45447]) +* Add Bitbucket and Gitlab browser guessing (in addition to existing +GitHub browser guessing) +(https://github.com/jenkinsci/git-plugin/pull/562[PR#562]) +* Validate Fisheye git browser URL during input +(https://issues.jenkins-ci.org/browse/JENKINS-48064[JENKINS-48064]) +* Allow retry by throwing IOException if submodule update fails +(https://issues.jenkins-ci.org/browse/JENKINS-32481[JENKINS-32481]) +* Don't pass empty username to User.get() +(https://issues.jenkins-ci.org/browse/JENKINS-48589[JENKINS-48589]) + +== Version 3.6.4 (November 5, 2017) + +* Add support for tagged pipeline shared libraries lost in 3.6.3 +(https://issues.jenkins-ci.org/browse/JENKINS-47824[JENKINS-47824]) + +== Version 3.6.3 (October 26, 2017) + +* Fix ssh based branch indexing failure with default credentials +(https://issues.jenkins-ci.org/browse/JENKINS-47629[JENKINS-47629], +https://issues.jenkins-ci.org/browse/JENKINS-47659[JENKINS-47659], +https://issues.jenkins-ci.org/browse/JENKINS-47680[JENKINS-47680]) + +== Version 3.6.2 (October 23, 2017) + +* Fix visibility of enum required to implement new API for +https://issues.jenkins-ci.org/browse/JENKINS-47526[JENKINS-47526] + +== Version 3.6.1 (October 23, 2017) + +* A merge conflict in PreBuildMerge will corrupt BuildData history in +previous builds +(https://issues.jenkins-ci.org/browse/JENKINS-44037[JENKINS-44037]) +* Allow up to 4 second time offset in Windows file systems +(https://github.com/jenkinsci/git-plugin/pull/536[PR#536]) +* Improve test coverage +(https://github.com/jenkinsci/git-plugin/pull/537[PR#537], +https://github.com/jenkinsci/git-plugin/pull/539[PR#539], +https://github.com/jenkinsci/git-plugin/pull/540[PR#540]) +* Fix incorrect activation of tag categories (the tag category was +enabled in all the right situations but as a result of the wrong test) +(https://github.com/jenkinsci/git-plugin/pull/541[PR#541]) +* Remove duplicate code in AbstractGitSCMSource +(https://github.com/jenkinsci/git-plugin/pull/542[PR#542]) +* Optimize operations that do not require a local repository cache +(https://github.com/jenkinsci/git-plugin/pull/544[PR#544]) +* Resolve parameters in UserMergeOptions +(https://github.com/jenkinsci/git-plugin/pull/522[PR#522]) +* Provide an API to allow avoiding local repository cache for +GitSCMSource +(https://issues.jenkins-ci.org/browse/JENKINS-47526[JENKINS-47526]) +* Change the UI for Advanced Clone Behaviours to avoid confusing +"negative" fetch tags label +(https://issues.jenkins-ci.org/browse/JENKINS-45822[JENKINS-45822]) + +== Version 3.6.0 (October 2, 2017) + +* Allow traits to support tag discovery +(https://issues.jenkins-ci.org/browse/JENKINS-46207[JENKINS-46207]) +* Don't exceed response header length +(https://issues.jenkins-ci.org/browse/JENKINS-46929[JENKINS-46929]) +* Don't fail build if diagnostic print of commit message fails +(https://issues.jenkins-ci.org/browse/JENKINS-45729[JENKINS-45729]) + +== Version 3.5.1 (August 5, 2017) + +* Extend API for Blue Ocean pipeline editing support in git +* Extend API to allow PreBuildMerge trait through a new plugin +* Don't ignore branches with '/' in GitSCMFileSystem +(https://issues.jenkins-ci.org/browse/JENKINS-42817[JENKINS-42817]) +* Show folder scoped credentials in modern SCM +(https://issues.jenkins-ci.org/browse/JENKINS-44271[JENKINS-44271]) + +== Version 3.5.0 (July 28, 2017) + +* Upgrade to version 2.5.0 +* Switch GitSCMSource indexing based on ls-remote to correctly +determine orphaned branches +(https://issues.jenkins-ci.org/browse/JENKINS-44751[JENKINS-44751]) +* (Internal, not user visible) Provide an extension for downstream +SCMSource plugins to use for PR merging that disables shallow clones +when doing a PR-merge +(https://issues.jenkins-ci.org/browse/JENKINS-45771[JENKINS-45771]) + +== Version 3.4.1 (July 18, 2017) + +* Fix credentials field being incorrectly marked as transient +(https://issues.jenkins-ci.org/browse/JENKINS-45598[JENKINS-45598]) + +== Version 3.4.0 (July 17, 2017) + +* Refactor the Git Branch Source UI / UX to simplify configuration +and enable configuration options to be shared with dependent plugins +such as GitHub Branch Source and Bitbucket Branch Source +(https://issues.jenkins-ci.org/browse/JENKINS-43507[JENKINS-43507]). +Please consult the linked ticket for full details. The high-level +changes are: + +** There were a number of behaviours that are valid when used from a +standalone job but are not valid in the context of a branch source and a +multibranch project. These behaviours did not (and could not) work when +configured against a branch source. These behaviours have been removed +as configuration options for a Git Branch Source. +** In the context of a multibranch project, the checkout to local branch +behaviour will now just check out to the branch name that matches the +name of the branch. The ability to specify a fixed custom branch name +does not make sense in the context of a multibranch project. +** Because each branch job in a multibranch project will only ever build +the one specific branch, the default behaviour for a Git Branch Source +is now to use a minimal refspec corresponding to just the required +branch. Tags will not be checked out by default. If you have a +multibranch project that requires the full set of ref-specs (for +example, you might have a pipeline that will use some analysis tool on +the diff with some other branch) you can restore the previous behaviour +by adding the "Advanced Clone Behaviours". Note: In some cases you may +also need to add the "Specify ref specs" behaviour. + +== Version 3.3.2 (July 10, 2017) + +* Fix security issue +(https://jenkins.io/security/advisory/2017-07-10/[security advisory]) + +== Version 3.3.1 (June 23, 2017) + +* Print first line of commit message in console log +(https://issues.jenkins-ci.org/browse/JENKINS-38241[JENKINS-38241], +https://issues.jenkins-ci.org/browse/JENKINS-38827[JENKINS-38827]) +* Allow scm steps to return revision +(https://issues.jenkins-ci.org/browse/JENKINS-26100[JENKINS-26100], +https://issues.jenkins-ci.org/browse/JENKINS-38827[JENKINS-38827)]) +* Don't require crumb for POST to /git/notifyCommit even when CSRF is +enabled +(https://issues.jenkins-ci.org/browse/JENKINS-34350[JENKINS-34350]) +* Fix credentials tracking null pointer exception in pipeline library +use (https://issues.jenkins-ci.org/browse/JENKINS-44640[JENKINS-44640]) +* Fix credentials tracking null pointer exception in git parameters +use (https://issues.jenkins-ci.org/browse/JENKINS-44087[JENKINS-44087]) + +== Version 3.3.0 (April 21, 2017) + +* Track credentials use so that credentials show the jobs which use +them (https://issues.jenkins-ci.org/browse/JENKINS-38827[JENKINS-38827]) +* Add a "Branches" list view column +(https://issues.jenkins-ci.org/browse/JENKINS-37331[JENKINS-37331]) +* Add some Italian localization +* Fix null pointer exception when pipeline definition includes a +branch with no repository +(https://issues.jenkins-ci.org/browse/JENKINS-43630[JENKINS-43630]) + +== Version 3.2.0 (March 28, 2017) + +* Add reporting API for default remote branch (https://issues.jenkins-ci.org/browse/JENKINS-40834[JENKINS-40834]) +* Remove extra git tag actions from build results sidebar +(https://issues.jenkins-ci.org/browse/JENKINS-35475[JENKINS-35475]) + +== Version 3.1.0 (March 4, 2017) + +* Add command line git https://git-lfs.github.com/[large file support (LFS)] +(https://issues.jenkins-ci.org/browse/JENKINS-30318[JENKINS-30318], +https://issues.jenkins-ci.org/browse/JENKINS-35687[JENKINS-35687], +https://issues.jenkins-ci.org/browse/JENKINS-38708[JENKINS-38708], +https://issues.jenkins-ci.org/browse/JENKINS-40174[JENKINS-40174]) +* Allow custom remote and refspec for GitSCMSource (https://issues.jenkins-ci.org/browse/JENKINS-40908[JENKINS-40908]) +* Add help for GitSCMSource (https://issues.jenkins-ci.org/browse/JENKINS-42204[JENKINS-42204]) +* Add help for multiple refspecs (https://issues.jenkins-ci.org/browse/JENKINS-42050[JENKINS-42050]) +* Log a warning if buildsByBranchName is too large (https://issues.jenkins-ci.org/browse/JENKINS-19022[JENKINS-19022]) +* Avoid incorrect triggers when processing events (https://issues.jenkins-ci.org/browse/JENKINS-42236[JENKINS-42236]) + +== Version 3.0.5 (February 9, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Upgrade SCM API dependency to 2.0.3 +* Expose event origin to listeners +(https://issues.jenkins-ci.org/browse/JENKINS-41812[JENKINS-41812]) + +== Version 2.6.5 (February 9, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Upgrade SCM API dependency to 2.0.3 +* Expose event origin to listeners +(https://issues.jenkins-ci.org/browse/JENKINS-41812[JENKINS-41812]) + +== Version 3.0.4 (February 2, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Upgrade to latest SCM API dependency + +== Version 2.6.4 (February 2, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Upgrade to latest SCM API dependency +* Remove beta dependency that was left by mistake in the 2.6.2 +release (this is what 2.6.2 should have been) + +== Version 3.0.3 (January 16, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Remove beta dependency that was left by mistake in the 3.0.2 +release (this is what 3.0.2 should have been) + +== Version 2.6.3 (SKIPPED) + +* This version number has been skipped to keep alignment of the patch +version with the 3.0.x line until the SCM API coordinated releases have +been published to the update center + +== Version 3.0.2 (January 16, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Fix potential NPE in matching credentials +(https://github.com/jenkinsci/git-plugin/pull/467[PR #467]) +* Add API to allow plugins to configure the SCM browser after +instantiation +(https://issues.jenkins-ci.org/browse/JENKINS-39837[JENKINS-39837]) +* Updated Japanese translations +* Upgrade to SCM API 2.0.x APIs +(https://issues.jenkins-ci.org/browse/JENKINS-39355[JENKINS-39355]) +* Fix help text (https://github.com/jenkinsci/git-plugin/pull/451[PR#451]) + +== Version 2.6.2 (January 16, 2017) + +* Please read https://jenkins.io/blog/2017/01/17/scm-api-2/[this Blog Post] before upgrading +* Allow the SCM browser to be configured after SCM instance created +(https://issues.jenkins-ci.org/browse/JENKINS-39837[JENKINS-39837]) +* Fixed translations +* Fixed copyright +* Updated Japanese translation +* Upgrade to SCM API 2.0.x APIs +(https://issues.jenkins-ci.org/browse/JENKINS-39355[JENKINS-39355]) +* API to get author or committer email without having to call +getAuthor() + +== Version 3.0.2-beta-1 (December 16, 2016) + +* Update to SCM-API 2.0.1 APIs +(https://issues.jenkins-ci.org/browse/JENKINS-39355[JENKINS-39355]) +* Add implementation of SCMFileSystem +(https://issues.jenkins-ci.org/browse/JENKINS-40382[JENKINS-40382]) +* Fix help text for excluded regions regex +(https://github.com/jenkinsci/git-plugin/pull/451[PR#451]) + +== Version 2.6.2-beta-1 (December 16, 2016) + +* Update to SCM-API 2.0.1 APIs +(https://issues.jenkins-ci.org/browse/JENKINS-39355[JENKINS-39355]) +* Add implementation of SCMFileSystem +(https://issues.jenkins-ci.org/browse/JENKINS-40382[JENKINS-40382]) + +== Version 3.0.1 (November 18, 2016) + +* Allow retrieval of a single revision (for improved pipeline support) +(https://issues.jenkins-ci.org/browse/JENKINS-31155[JENKINS-31155]) +* Avoid null pointer exception in prebuild use of build data +(https://issues.jenkins-ci.org/browse/JENKINS-34369[JENKINS-34369]) +* Allow git credentials references from global configuration screens +(https://issues.jenkins-ci.org/browse/JENKINS-38048[JENKINS-38048]) +* Use correct specific version in workflow pipeline on subsequent +builds +(https://github.com/jenkinsci/git-plugin/commit/e15a431a62781c6081c57354a33a7e148a4452a1[e15a43]) + +== Version 2.6.1 (November 9, 2016) + +* Allow retrieval of a single revision (for improved pipeline support) +(https://issues.jenkins-ci.org/browse/JENKINS-31155[JENKINS-31155]) +* Avoid null pointer exception in prebuild use of build data +(https://issues.jenkins-ci.org/browse/JENKINS-34369[JENKINS-34369]) +* Allow git credentials references from global configuration screens +(https://issues.jenkins-ci.org/browse/JENKINS-38048[JENKINS-38048]) +* Use correct specific version in workflow pipeline on subsequent +builds +(https://github.com/jenkinsci/git-plugin/commit/e15a431a62781c6081c57354a33a7e148a4452a1[e15a43]) + +== Version 3.0.0 (September 10, 2016) + +* Add submodule authentication using same credentials as parent +repository (https://issues.jenkins-ci.org/browse/JENKINS-20941[JENKINS-20941]) +* Require JDK 7 and Jenkins 1.625 as minimum Jenkins version + +== Version 2.6.0 (September 2, 2016) + +* Add command line git support to multi-branch pipeline jobs +(https://issues.jenkins-ci.org/browse/JENKINS-33983[JENKINS-33983], +https://issues.jenkins-ci.org/browse/JENKINS-35565[JENKINS-35565], +https://issues.jenkins-ci.org/browse/JENKINS-35567[JENKINS-35567], +https://issues.jenkins-ci.org/browse/JENKINS-36958[JENKINS-36958], +https://issues.jenkins-ci.org/browse/JENKINS-37297[JENKINS-37297]) +* Remove deleted branches from multi-branch cache when using command +line git (https://issues.jenkins-ci.org/browse/JENKINS-37727[JENKINS-37727]) +* Create multi-branch cache parent directories if needed +(https://issues.jenkins-ci.org/browse/JENKINS-37482[JENKINS-37482]) +* Use credentials API 2.1 (https://issues.jenkins-ci.org/browse/JENKINS-35525[JENKINS-35525]) + +== Version 2.5.3 (July 30, 2016) + +* Prepare to coexist with git client plugin 2.0 when it changes from +JGit 3 to JGit 4 +(https://github.com/jenkinsci/git-plugin/commit/71946a2896d3adcd1171ac59b7c45bacaf7a9c56[commit]) +* Fix gogs repository browser configuration (https://issues.jenkins-ci.org/browse/JENKINS-37066[JENKINS-37066]) +* Optionally "honor refspec on initial clone" rather than always +honoring refspec on initial clone (https://issues.jenkins-ci.org/browse/JENKINS-36507[JENKINS-36507]) +* Don't ignore the checkout timeout value (https://issues.jenkins-ci.org/browse/JENKINS-22547[JENKINS-22547]) + +== Version 3.0.0-beta2 (July 6, 2016) + +* Fix compatibility break introduced by git plugin 2.5.1 release +(https://issues.jenkins-ci.org/browse/JENKINS-36419[JENKINS-36419]) +* Add many more git options to multi-branch project plugin and +literate plugin (plugins which use GitSCMSource) +* Improved help for regex branch specifiers and branch name matching +* Improve github browser guesser for more forms of GitHub URL +* Use Jenkins common controls for numeric entry in fields which are +limited to numbers (like shallow clone depth). Blocks the user from +inserting alphabetic characters into a field which should take numbers +* Honor refspec on initial fetch (https://issues.jenkins-ci.org/browse/JENKINS-31393[JENKINS-31393]) (note, some users may +depend on the old, poor behavior that the plugin fetched all refspecs +even though the user had specified a narrower refspec. Those users can +delete their refspec or modify it to be as wide as they need) +* Disallow deletion of the last repository entry in git configuration +(https://issues.jenkins-ci.org/browse/JENKINS-33956[JENKINS-33956]) + +== Version 2.5.2 (July 4, 2016) + +* Fix compatibility break introduced by git plugin 2.5.1 release +(https://issues.jenkins-ci.org/browse/JENKINS-36419[JENKINS-36419]) + +== Version 2.5.1 (July 2, 2016) + +* Add many more git options to multi-branch project plugin and +literate plugin (plugins which use GitSCMSource) +* Improved help for regex branch specifiers and branch name matching +* Improve github browser guesser for more forms of GitHub URL +* Use Jenkins common controls for numeric entry in fields which are +limited to numbers (like shallow clone depth). Blocks the user from +inserting alphabetic characters into a field which should take numbers +* Honor refspec on initial fetch (https://issues.jenkins-ci.org/browse/JENKINS-31393[JENKINS-31393]) (note, some users may +depend on the old, poor behavior that the plugin fetched all refspecs +even though the user had specified a narrower refspec. Those users can +delete their refspec or modify it to be as wide as they need) +* Disallow deletion of the last repository entry in git configuration +(https://issues.jenkins-ci.org/browse/JENKINS-33956[JENKINS-33956]) + +== Version 2.5.0 (June 19, 2016) - Submodule authentication has moved into git 3.0.0-beta + +* Reject parameters passed through unauthenticated notifyCommit calls +(SECURITY-275) +* Don't generate error when two repos defined and specific SHA1 is +built (https://issues.jenkins-ci.org/browse/JENKINS-26268[JENKINS-26268]) +* Fix stack trace generated when AssemblaWeb used as git hosting +service +* Fix array index violation when e-mail address is single character +"@" +* Add support for gogs self-hosted git service +* Use environment from executing node rather than using environment +from master +* Move pipeline GitStep from pipeline plugin to git plugin +(https://issues.jenkins-ci.org/browse/JENKINS-35247[JENKINS-35247]); *note* that if you have the _Pipeline: SCM Step_ plugin +installed, you must update it as well + +== Version 3.0.0-beta1 (June 15, 2016) + +* Continuation of git plugin 2.5.0-beta series (2.5.0 release number +used for SECURITY-275 fix) +* Don't generate error when two repos defined and specific SHA1 is +built (https://issues.jenkins-ci.org/browse/JENKINS-26268[JENKINS-26268]) +* Fix stack trace generated when AssemblaWeb used as git hosting +service +* Fix array index violation when e-mail address is single character +"@" +* Add support for gogs self-hosted git service +* Use environment from executing node rather than using environment +from master +* Move pipeline GitStep from pipeline plugin to git plugin +(https://issues.jenkins-ci.org/browse/JENKINS-35247[JENKINS-35247]) + +== Version 2.5.0-beta5 (April 19, 2016) + +* Fix botched merge that was included in 2.5.0-beta4 +* Include latest changes from master branch (git plugin 2.4.4) + +== Version 2.4.4 (March 24, 2016) + +* Fix git plugin 2.4.3 data loss when saving job definition +(https://issues.jenkins-ci.org/browse/JENKINS-33695[JENKINS-33695] and https://issues.jenkins-ci.org/browse/JENKINS-33564[JENKINS-33564]) +* Restore BuildData.equals lost in git plugin 2.4.2 revert mistake +(https://issues.jenkins-ci.org/browse/JENKINS-29326[JENKINS-29326]) + +== Version 2.4.3 (March 19, 2016) + +* Optionally derive local branch name from remote branch name +(https://issues.jenkins-ci.org/browse/JENKINS-33202[JENKINS-33202]) +* Allow shallow clone depth to be specified (https://issues.jenkins-ci.org/browse/JENKINS-24728[JENKINS-24728]) +* Allow publishing from shallow clone if git version supports it +(https://issues.jenkins-ci.org/browse/JENKINS-31108[JENKINS-31108]) +* Allow GitHub browser guesser to work even if multiple refspecs +defined for same URL (https://issues.jenkins-ci.org/browse/JENKINS-33409[JENKINS-33409]) +* Clarify Team Foundation Server browser name (remove 2013 specific +string) +* Reduce memory use in difference calculation (https://issues.jenkins-ci.org/browse/JENKINS-31326[JENKINS-31326]) +* Resolve several findbugs warnings + +== Version 2.4.2 (February 1, 2016) + +* Show changelog even if prune stale branches is enabled +(https://issues.jenkins-ci.org/browse/JENKINS-29482[JENKINS-29482]) +* Set GIT_PREVIOUS_SUCCESSFUL_COMMIT even if prune stale branches is +enabled (https://issues.jenkins-ci.org/browse/JENKINS-32218[JENKINS-32218]) + +== Version 2.4.1 (December 26, 2015) + +* Allow clone to optionally not fetch tags (https://issues.jenkins-ci.org/browse/JENKINS-14572[JENKINS-14572]) +* Allow submodules to use a reference repo (https://issues.jenkins-ci.org/browse/JENKINS-18666[JENKINS-18666]) +* Use OR instead of AND when combining multiple refspecs +(https://issues.jenkins-ci.org/browse/JENKINS-29796[JENKINS-29796]) +* Remove dead branches from BuildData (https://issues.jenkins-ci.org/browse/JENKINS-29482[JENKINS-29482]) +* Fix Java 6 date parsing error (https://issues.jenkins-ci.org/browse/JENKINS-29857[JENKINS-29857]) +* Set changeset time correctly (https://issues.jenkins-ci.org/browse/JENKINS-30073[JENKINS-30073]) +* Include parent SHA1 in RhodeCode diff URL (https://issues.jenkins-ci.org/browse/JENKINS-17117[JENKINS-17117]) +* Don't set GIT_COMMIT to an empty value (https://issues.jenkins-ci.org/browse/JENKINS-27180[JENKINS-27180]) +* Fix AssemblaWeb diff link (https://issues.jenkins-ci.org/browse/JENKINS-29731[JENKINS-29731]) +* Attempt fix for multi-scm sporadic failures (https://issues.jenkins-ci.org/browse/JENKINS-26587[JENKINS-26587]) + +== Version 2.5.0-beta3 (November 12, 2015) + +* Still more work on submodule authentication support by allowing +submodules to use parent credentials (https://issues.jenkins-ci.org/browse/JENKINS-20941[JENKINS-20941]) + +== Version 2.5.0-beta2 (November 8, 2015) + +* More work on submodule authentication support by allowing submodules +to use parent credentials (https://issues.jenkins-ci.org/browse/JENKINS-20941[JENKINS-20941]) + +== Version 2.5.0-beta1 (November 4, 2015) + +* Submodule authentication support by allowing submodules to use +parent credentials (https://issues.jenkins-ci.org/browse/JENKINS-20941[JENKINS-20941]) + +== Version 2.4.0 (July 18, 2015) + +* Branch spec help text improved (https://issues.jenkins-ci.org/browse/JENKINS-27115[JENKINS-27115]) +* Allow additional notifyCommit arguments (https://issues.jenkins-ci.org/browse/JENKINS-27902[JENKINS-27902]) +* Parameterized branch name handling improvements (Pull requests 226, +308, 309, https://issues.jenkins-ci.org/browse/JENKINS-27327[JENKINS-27327], https://issues.jenkins-ci.org/browse/JENKINS-27351[JENKINS-27351], https://issues.jenkins-ci.org/browse/JENKINS-27352[JENKINS-27352]) +* Display error message in log when fetch fails (regression fix) +(https://issues.jenkins-ci.org/browse/JENKINS-26225[JENKINS-26225], https://issues.jenkins-ci.org/browse/JENKINS-27567[JENKINS-27567], https://issues.jenkins-ci.org/browse/JENKINS-27886[JENKINS-27886], https://issues.jenkins-ci.org/browse/JENKINS-28134[JENKINS-28134]) +* Fix IllegalStateException when using notifyCommit URL +(https://issues.jenkins-ci.org/browse/JENKINS-26582[JENKINS-26582]) +* Allow branch specification regex which does not include '*' +(https://issues.jenkins-ci.org/browse/JENKINS-26842[JENKINS-26842]) +* Detect changes correctly when polling +(https://issues.jenkins-ci.org/browse/JENKINS-27093[JENKINS-27093], +https://issues.jenkins-ci.org/browse/JENKINS-27332[JENKINS-27332], +https://issues.jenkins-ci.org/browse/JENKINS-27769[JENKINS-27769]) +* Fix GitHub Webhook handling (https://issues.jenkins-ci.org/browse/JENKINS-27282[JENKINS-27282]) +* Fix polling with a parameterized branch name (https://issues.jenkins-ci.org/browse/JENKINS-27349[JENKINS-27349]) +* Don't throw exception when changelog entry is missing parent +(https://issues.jenkins-ci.org/browse/JENKINS-28260[JENKINS-28260], +https://issues.jenkins-ci.org/browse/JENKINS-28290[JENKINS-28290], +https://issues.jenkins-ci.org/browse/JENKINS-28291[JENKINS-28291]) +* Don't throw exception when saving GitLab browser config +(https://issues.jenkins-ci.org/browse/JENKINS-28792[JENKINS-28792]) +* Rebuild happened on each poll, even with no changes (https://issues.jenkins-ci.org/browse/JENKINS-29066[JENKINS-29066]) +* Remote class loading issue work-around (https://issues.jenkins-ci.org/browse/JENKINS-21520[JENKINS-21520]) + +== Version 2.3.5 (February 18, 2015) + +* Support Microsoft Team Foundation Server 2013 as a git repository +browser +* Support more merge modes (fast forward, no fast forward, fast +forward only (https://issues.jenkins-ci.org/browse/JENKINS-12402[JENKINS-12402]) +* Handle regular expression branch name correctly even if it does not +contain asterisk (https://issues.jenkins-ci.org/browse/JENKINS-26842[JENKINS-26842]) +* Log the error stack trace if fetch fails (temporary diagnostic aid) + +== Version 2.3.4 (January 8, 2015) + +* Fix jelly page escape bug (which was visible in the GitHub plugin) + +== Version 2.2.12 (January 8, 2015) + +* Fix jelly page escape bug (which was visible in the GitHub plugin) + +== Version 2.3.3 (January 6, 2015) + +* Use git client plugin 1.15.0 +* Escape HTML generated into jelly pages with escape="true" +* Expand environment variables in GitPublisher again (https://issues.jenkins-ci.org/browse/JENKINS-24786[JENKINS-24786]) + +== Version 2.2.11 (January 6, 2015) + +* Update to JGit 3.6.1 +* Use git client plugin 1.15.0 +* Escape HTML generated into jelly pages with escape="true" +* Fix multiple builds can be triggered for same commit (https://issues.jenkins-ci.org/browse/JENKINS-25639[JENKINS-25639]) + +== Version 2.3.2 (December 19, 2014) + +* Use git client plugin 1.13.0 +(http://git-blame.blogspot.com.es/2014/12/git-1856-195-205-214-and-221-and.html[CVE-2014-9390]) + +== Version 2.2.10 (December 19, 2014) + +* Use git client plugin 1.13.0 +(http://git-blame.blogspot.com.es/2014/12/git-1856-195-205-214-and-221-and.html[CVE-2014-9390]) +* Do not continuously build when polling multiple repositories +(https://issues.jenkins-ci.org/browse/JENKINS-25639[JENKINS-25639]) + +== Version 2.3.1 (November 29, 2014) + +* Add a build chooser to limit branches to be built based on age or +ancestor SHA1 +* Update to git-client-plugin 1.12.0 (includes JGit 3.5.2) +* Allow polling to ignore detected changes based on commit content +* Do not continuously build when polling multiple repositories +(https://issues.jenkins-ci.org/browse/JENKINS-25639[JENKINS-25639]) +* Expand parameters on repository url before associate one url to one +credential (https://issues.jenkins-ci.org/browse/JENKINS-23675[JENKINS-23675]) +* Expand parameters on branch spec for remote polling (https://issues.jenkins-ci.org/browse/JENKINS-20427[JENKINS-20427], +https://issues.jenkins-ci.org/browse/JENKINS-14276[JENKINS-14276]) +* Fix Gitiles file link for various Gitiles versions (https://issues.jenkins-ci.org/browse/JENKINS-25568[JENKINS-25568]) +* Fixed notifyCommit builddata (https://issues.jenkins-ci.org/browse/JENKINS-24133[JENKINS-24133]) +* Improve notifyCommit message to reduce user confusion + +== Version 2.2.9 (November 23, 2014) + +* Added behavior: "Polling ignores commits with certain messages" +* GIT_BRANCH set to detached when sha1 parameter set in notifyCommit +URL (https://issues.jenkins-ci.org/browse/JENKINS-24133[JENKINS-24133]) + +== Version 2.2.8 (November 12, 2014) + +* Add submodule update timeout as an option (https://issues.jenkins-ci.org/browse/JENKINS-22400[JENKINS-22400]) +* Update Gitlab support for newer Gitlab versions (https://issues.jenkins-ci.org/browse/JENKINS-25568[JENKINS-25568]) +* No exception if changeset author can't be found (https://issues.jenkins-ci.org/browse/JENKINS-16737[JENKINS-16737] and +https://issues.jenkins-ci.org/browse/JENKINS-10434[JENKINS-10434]) +* Annotate builddata earlier to reduce race conditions (https://issues.jenkins-ci.org/browse/JENKINS-23641[JENKINS-23641]) +* Pass marked revision to decorate revision (https://issues.jenkins-ci.org/browse/JENKINS-25191[JENKINS-25191]) +* Avoid null pointer exception when last repo or branch deleted +(https://issues.jenkins-ci.org/browse/JENKINS-25313[JENKINS-25313]) +* Allow retry by throwing a different exception during certain fetch +failures (https://issues.jenkins-ci.org/browse/JENKINS-20531[JENKINS-20531]) +* Do not require a workspace when polling multiple repositories +(https://issues.jenkins-ci.org/browse/JENKINS-25414[JENKINS-25414]) + +== Version 2.3 (November 10, 2014) + +* Released for Jenkins 1.568 and later, update center will exclude +from earlier Jenkins versions +* Do not require a workspace when polling multiple repositories +(https://issues.jenkins-ci.org/browse/JENKINS-25414[JENKINS-25414]) + +== Version 2.3-beta-4 (October 29, 2014) + +* Update to JGit 3.5.1 +* Allow retry if fetch fails (https://issues.jenkins-ci.org/browse/JENKINS-20531[JENKINS-20531]) +* Don't NPE if all repos and all branches removed from job definition +(https://issues.jenkins-ci.org/browse/JENKINS-25313[JENKINS-25313]) +* Correctly record built revision even on failed merge (https://issues.jenkins-ci.org/browse/JENKINS-25191[JENKINS-25191]) +* Record build data sooner for better concurrency and safety +(https://issues.jenkins-ci.org/browse/JENKINS-23641[JENKINS-23641]) +* Do not throw exception if author can't be found in change set +(https://issues.jenkins-ci.org/browse/JENKINS-16737[JENKINS-16737], https://issues.jenkins-ci.org/browse/JENKINS-10434[JENKINS-10434]) + +== Version 2.2.7 (October 8, 2014) + +* Honor project specific Item/CONFIGURE permission even if overall +Item/CONFIGURE has not been granted (SECURITY-158) +* Save current build in BuildData prior to rescheduling +(https://issues.jenkins-ci.org/browse/JENKINS-21464[JENKINS-21464]) +* Fix GitPublisher null pointer exception when previous slave is +missing +* Expand variables in branch spec for remote polling (https://issues.jenkins-ci.org/browse/JENKINS-20427[JENKINS-20427], +https://issues.jenkins-ci.org/browse/JENKINS-14276[JENKINS-14276]) +* Add GIT_PREVIOUS_SUCCESSFUL_COMMIT environment variable + +== Version 2.3-beta-3 (October 8, 2014) + +* Honor project specific Item/CONFIGURE permission even if overall +Item/CONFIGURE has not been granted (SECURITY-158) +* Save current build in BuildData prior to rescheduling +(https://issues.jenkins-ci.org/browse/JENKINS-21464[JENKINS-21464]) +* Fix GitPublisher null pointer exception when previous slave is +missing +* Expand variables in branch spec for remote polling (https://issues.jenkins-ci.org/browse/JENKINS-20427[JENKINS-20427], +https://issues.jenkins-ci.org/browse/JENKINS-14276[JENKINS-14276]) +* Add GIT_PREVIOUS_SUCCESSFUL_COMMIT environment variable + +== Version 2.2.6 (September 20, 2014) + +* Add optional "force" to push from publisher (https://issues.jenkins-ci.org/browse/JENKINS-24082[JENKINS-24082]) +* Support gitlist as a repository browser (https://issues.jenkins-ci.org/browse/JENKINS-19029[JENKINS-19029]) +* Print the remote HEAD SHA1 in poll results to ease diagnostics +* Add help describing the regex syntax allowed for "Branches to build" +* Improve environment support which caused git polling to fail with +"ssh not found" (https://issues.jenkins-ci.org/browse/JENKINS-24516[JENKINS-24516], https://issues.jenkins-ci.org/browse/JENKINS-24467[JENKINS-24467]) +* Pass a listener to calls to getEnvironment (https://issues.jenkins-ci.org/browse/JENKINS-24772[JENKINS-24772]) + +== Version 2.3-beta-2 (September 3, 2014) + +* Print remote head when fetching a SHA1 +* Assembla browser breaks config page (https://issues.jenkins-ci.org/browse/JENKINS-24261[JENKINS-24261]) +* Recent changes is always empty in merge job (https://issues.jenkins-ci.org/browse/JENKINS-20392[JENKINS-20392]) +* Polling incorrectly detects changes when refspec contains variable +(https://issues.jenkins-ci.org/browse/JENKINS-22009[JENKINS-22009]) +* Matrix project fails pre-merge (https://issues.jenkins-ci.org/browse/JENKINS-23179[JENKINS-23179]) +* Add "Change log compare to branch" option to improve "Recent +changes" for certain use cases +* Add Assembla as supported source code and change browser support +* Add Gitiles as supported source code and change browser support +(android project git browser) +* Return correct date/time to REST query of build date (https://issues.jenkins-ci.org/browse/JENKINS-23791[JENKINS-23791]) +* Add timeout option to checkout (for slow file systems and large +repos) (https://issues.jenkins-ci.org/browse/JENKINS-22400[JENKINS-22400]) +* Expand parameters on repository url before evaluating credentials +(https://issues.jenkins-ci.org/browse/JENKINS-23675[JENKINS-23675]) +* Update to git-client-plugin 1.10.1.0 and JGit 3.4.1 +* Update other dependencies (ssh-credentials, credentials, +httpcomponents, joda-time) + +== Version 2.2.5 (August 15, 2014) + +* Assembla browser breaks config page (https://issues.jenkins-ci.org/browse/JENKINS-24261[JENKINS-24261]) +* Recent changes is always empty in merge job (https://issues.jenkins-ci.org/browse/JENKINS-20392[JENKINS-20392]) +* Polling incorrectly detects changes when refspec contains variable +(https://issues.jenkins-ci.org/browse/JENKINS-22009[JENKINS-22009]) +* Matrix project fails pre-merge (https://issues.jenkins-ci.org/browse/JENKINS-23179[JENKINS-23179]) + +== Version 2.2.4 (August 2, 2014) + +* Add "Change log compare to branch" option to improve "Recent +changes" for certain use cases +* Add Assembla as supported source code and change browser support +* Add Gitiles as supported source code and change browser support +(android project git browser) +* Return correct date/time to REST query of build date +(https://issues.jenkins-ci.org/browse/JENKINS-23791[JENKINS-23791]) + +== Version 2.2.3 (July 31, 2014) + +* Add timeout option to checkout (for slow file systems and large +repos) (https://issues.jenkins-ci.org/browse/JENKINS-22400[JENKINS-22400]) +* Expand parameters on repository url before evaluating credentials +(https://issues.jenkins-ci.org/browse/JENKINS-23675[JENKINS-23675]) +* Update to git-client-plugin 1.10.1.0 and JGit 3.4.1 +* Update other dependencies (ssh-credentials, credentials, +httpcomponents, joda-time) + +== Version 2.3-beta-1 (June 16, 2014) + +* Adapting to SCM API changes in Jenkins 1.568+. (https://issues.jenkins-ci.org/browse/JENKINS-23365[JENKINS-23365]) +* Fixed advanced branch spec behaviour in getCandidateRevisions +* includes/excludes branches specified using wildcard, and separated +by white spaces. +* Update to git-client-plugin 1.9.0 and JGit 3.4.0 +* Option to set submodules update timeout (https://issues.jenkins-ci.org/browse/JENKINS-22400[JENKINS-22400]) + +== Version 2.2.2 (June 24, 2014) + +* Remote API export problem finally fixed (https://issues.jenkins-ci.org/browse/JENKINS-9843[JENKINS-9843]) + +== Version 2.2.1 (April 12, 2014) + +* Allow clean before checkout (https://issues.jenkins-ci.org/browse/JENKINS-22510[JENKINS-22510]) +* Do not append trailing slash to most repository browser URL's +(https://issues.jenkins-ci.org/browse/JENKINS-22342[JENKINS-22342]) +* Fix null pointer exception in git polling with inverse build chooser +(https://issues.jenkins-ci.org/browse/JENKINS-22053[JENKINS-22053]) + +== Version 2.2.0 (April 4, 2014) + +* Add optional submodule remote tracking if git version newer than +1.8.2 (https://issues.jenkins-ci.org/browse/JENKINS-19468[JENKINS-19468]) +* Update to JGit 3.3.1 +* Fix javadoc warnings + +== Version 2.1.0 (March 31, 2014) + +* Support sparse checkout if git version newer than 1.8.2 +(https://issues.jenkins-ci.org/browse/JENKINS-21809[JENKINS-21809]) +* Improve performance when many branches are in the repository +(https://issues.jenkins-ci.org/browse/JENKINS-5724[JENKINS-5724]) +* Retain git browser URL when saving job configuration +(https://issues.jenkins-ci.org/browse/JENKINS-22064[JENKINS-22064]) +* Resolve tags which contain slashes (https://issues.jenkins-ci.org/browse/JENKINS-21952[JENKINS-21952]) + +== Version 2.0.4 (March 6, 2014) + +* Allow extension to require workspace for polling (https://issues.jenkins-ci.org/browse/JENKINS-19001[JENKINS-19001]) +* ??? (tbd) + +== Version 2.0.3 (February 21, 2014) + +* Fix the post-commit hook notification logic (according +to http://javadoc.jenkins-ci.org/hudson/triggers/SCMTrigger.html#isIgnorePostCommitHooks()[SCMTrigger.html#isIgnorePostCommitHooks]) + +== Version 2.0.2 (February 20, 2014) + +* Option to configure timeout on major git operations (clone, fetch) +* Locks are considered a retryable failure +* notifyCommit now accept a sha1 - make commit hook design simpler and +more efficient (no poll required) +* Extend branch specifier (https://issues.jenkins-ci.org/browse/JENKINS-17417[JENKINS-17417]) and git repository URL +* Better support for branches with "/" in name (https://issues.jenkins-ci.org/browse/JENKINS-14026[JENKINS-14026]) +* Improve backward compatibility (https://issues.jenkins-ci.org/browse/JENKINS-20861[JENKINS-20861]) + +== Version 2.0.1 (January 8, 2014) + +* Use git-credentials-store so http credentials don't appear in +workspace (https://issues.jenkins-ci.org/browse/JENKINS-20318[JENKINS-20318]) +* Prune branch during fetch (https://issues.jenkins-ci.org/browse/JENKINS-20258[JENKINS-20258]) +* Fix migration for 1.x skiptag option (https://issues.jenkins-ci.org/browse/JENKINS-20561[JENKINS-20561]) +* Enforce Refsepc configuration after clone (https://issues.jenkins-ci.org/browse/JENKINS-20502[JENKINS-20502]) + +== Version 2.0 (October 22, 2013) + +* Refactored git plugin for UI to keep clean. Most exotic features +now are isolated in Extensions, that is the recommended way to introduce +new features +* Introduce support for credentials (both ssh and username/password) +based on credentials plugin + +== Version 1.5.0 (August 28, 2013) + +* Additional environmental values available to git notes +* Extension point for other plugin to receive commit notifications +* Support promoted builds plugin (passing GitRevisionParameter) +* Do not re-use last build's environment for remote polling +(https://issues.jenkins-ci.org/browse/JENKINS-14321[JENKINS-14321]) +* Fixed variable expansion during polling (https://issues.jenkins-ci.org/browse/JENKINS-7411[JENKINS-7411]) +* Added Phabricator and Kiln Harmony repository browsers, fixed +GitLab URLs + +== Version 1.4.0 (May 13, 2013) + +* Avoid spaces in tag name, rejected by JGit (https://issues.jenkins-ci.org/browse/JENKINS-17195[JENKINS-17195]) +* Force UTF-8 encoding to read changelog file (https://issues.jenkins-ci.org/browse/JENKINS-6203[JENKINS-6203]) +* Retry build if SCM retry is configured +(https://issues.jenkins-ci.org/browse/https://issues.jenkins-ci.org/browse/JENKINS-14575[JENKINS-14575]) +* Allow merge results to push from slave nodes, not just from master +node (https://issues.jenkins-ci.org/browse/https://issues.jenkins-ci.org/browse/JENKINS-16941[JENKINS-16941]) + +== Version 1.3.0 (March 12, 2013) + +* Fix a regression fetching from multiple remote repositories +(https://issues.jenkins-ci.org/browse/JENKINS-16914[JENKINS-16914]) +* Fix stackoverflow recursive invocation error caused by +MailAddressResolver (https://issues.jenkins-ci.org/browse/JENKINS-16849[JENKINS-16849]) +* Fix invalid id computing merge changelog (https://issues.jenkins-ci.org/browse/JENKINS-16888[JENKINS-16888]) +* Fix lock on repository files (https://issues.jenkins-ci.org/browse/JENKINS-12188[JENKINS-12188]) +* Use default git installation if none matches (https://issues.jenkins-ci.org/browse/JENKINS-17013[JENKINS-17013]). +* Expand _reference_ parameter when set with variables +* Expose GIT_URL environment variable (https://issues.jenkins-ci.org/browse/JENKINS-16684[JENKINS-16684]) +* Branch can be set by a regexp, starting with a colon (pull request +#138) + +== Version 1.2.0 (February 20, 2013) + +* move git client related stuff into Git Client plugin +* double checked backward compatibility with gerrit, git-parameter and +cloudbees validated-merge plugins. + +== Version 1.1.29 (February 17, 2013) + +* fix a regression that breaks jenkins remoting +* restore BuildChooser API signature, that introduced https://issues.jenkins-ci.org/browse/JENKINS-16851[JENKINS-16851] + +== Version 1.1.27 (February 17, 2013) + +* add version field to support new GitLab URL-scheme +* Trim branch name - a valid branch name does not begin or end with +whitespace. (https://issues.jenkins-ci.org/browse/JENKINS-15235[JENKINS-15235]) +* set changeSet.kind to "git" +* Avoid some calls to "git show" +* Fix checking for an email address (https://issues.jenkins-ci.org/browse/JENKINS-16453[JENKINS-16453]) +* update Git logo icon +* Pass combineCommits to action (https://issues.jenkins-ci.org/browse/JENKINS-15160[JENKINS-15160]) +* expose previous built commit from same branch as GIT_PREVIOUS_COMMIT +* re-schedule project when multiple candidate revisions are left +* expand parameters in the remote branch name of merge options + +=== GitAPI cleanup + +Long term plan is to replace GitAPI cli-based implementation with a pure +java (JGit) one, so that plugin is not system dependent. + +* move git-plugin specific logic in GitSCM, have GitAPI implementation +handle git client stuff only +* removed unused methods +* create unit test suite for GitAPI +* create alternate GitAPI implementation based on JGit + +== Version 1.1.26 (November 13, 2012) + +* git polling mechanism can have build in infinite loop (https://issues.jenkins-ci.org/browse/JENKINS-15803[JENKINS-15803]) + +== Version 1.1.25 (October 13, 2012) + +* Do "git reset" when we do "git clean" on git submodules +(https://github.com/jenkinsci/git-plugin/pull/100[pull #100]) +* NullPointerException during tag publishing (https://issues.jenkins-ci.org/browse/JENKINS-15391[JENKINS-15391]) +* Adds http://rhodecode.org/[RhodeCode] support (https://issues.jenkins-ci.org/browse/JENKINS-15420[JENKINS-15420]) +* Improved the `+BuildChooser+` extension point for other plugins. + +== Version 1.1.24 (September 27, 2012) + +* Shorten build data display name +https://issues.jenkins-ci.org/browse/https://issues.jenkins-ci.org/browse/JENKINS-15048[JENKINS-15048][issue #15048] +* Use correct refspec when fetching submodules +https://issues.jenkins-ci.org/browse/https://issues.jenkins-ci.org/browse/JENKINS-8149[JENKINS-8149][issue #8149] +* Allow a message to be associated with a tag created by the plugin + +== Version 1.1.23 (September 3, 2012) + +* Improve changelog parsing for merge targets +* prevent process to hang when git waits for user to interactively +provide credentials +* option to create a shallow clone to reduce network usage cloning large +git repositories +* option to use committer/author email as ID in jenkins user database +when parsing changelog (needed for openID / SSO integration) +* validate repository URL on job configuration + +== Version 1.1.22 (August 8, 2012) + +* Fix regression for fully qualified branch name (REPOSITORY/BRANCH) +https://issues.jenkins-ci.org/browse/JENKINS-14480[JENKINS-14480] +* Add support for variable expansion on branch spec (not just job +parameters) https://issues.jenkins-ci.org/browse/JENKINS-8563[JENKINS-8563] +* Use master environment, not last build node, for fast remote polling +https://issues.jenkins-ci.org/browse/JENKINS-14321[JENKINS-14321] +* run reset --hard on clean to take care of any local artifact +* normalize maven repository ID https://issues.jenkins-ci.org/browse/JENKINS-14443[JENKINS-14443] + +== Version 1.1.21 (July 10, 2012) + +* Fixed support for "/" in branches names (https://issues.jenkins-ci.org/browse/JENKINS-14026[JENKINS-14026]) +* Fixed issue on windows+msysgit to escape "^" on git command line +(https://issues.jenkins-ci.org/browse/JENKINS-13007[JENKINS-13007]) + +== Version 1.1.20 (June 25, 2012) + +* Fixed NPE (https://issues.jenkins-ci.org/browse/JENKINS-10880[JENKINS-10880]) +* Fixed a git-rev-parse problem on Windows (https://issues.jenkins-ci.org/browse/JENKINS-13007[JENKINS-13007]) +* Use 'git whatchanged' instead of 'git show' (https://issues.jenkins-ci.org/browse/JENKINS-13580[JENKINS-13580]) +* Added git note support + +== Version 1.1.19 (June 8, 2012) + +* restore GitAPI constructor for backward compatibility (https://issues.jenkins-ci.org/browse/JENKINS-12025[JENKINS-12025]) +* CGit browser support (https://issues.jenkins-ci.org/browse/JENKINS-6963[JENKINS-6963]). +* Handle special meaning of some characters on Windows (https://issues.jenkins-ci.org/browse/JENKINS-13007[JENKINS-13007]) +* fixed java.lang.NoSuchMethodError: java/lang/String.isEmpty() +(https://issues.jenkins-ci.org/browse/JENKINS-13993[JENKINS-13993]). +* Git icon(git-48x48.png) missing in job page. (https://issues.jenkins-ci.org/browse/JENKINS-13413[JENKINS-13413]). +* Git "Tag to push" should trim whitespace (https://issues.jenkins-ci.org/browse/JENKINS-13550[JENKINS-13550]). + +== Version 1.1.18 (April 27, 2012) + +* Loosened the repository matching algorithm for the push notification +to better work with a repository with multiple access protocols. + +== Version 1.1.17 (April 9, 2012) + +* Fixed NPE in `+compareRemoteRevisionWith+` (https://issues.jenkins-ci.org/browse/JENKINS-10880[JENKINS-10880]) +* Improved the caching of static resources +* `+notifyCommit+` endpoint now accept a comma delimited list of +affected branches. Only the build(s) that match those branches will be +triggered + +== Version 1.1.16 (February 28, 2012) + +* You can look up builds by their SHA1 through URLs like +\http://yourserver/jenkins/job/foo/scm/bySHA1/ab1249ab/ (any prefix of +SHA1 will work) +* Perform environment variable expansion on the checkout directory. +* Support GitLab scm browser +* Support BitBucket.org scm browser +* option to set includes regions (https://issues.jenkins-ci.org/browse/JENKINS-11749[JENKINS-11749]) +* fix regression to deserialize build history (https://issues.jenkins-ci.org/browse/JENKINS-12369[JENKINS-12369]) + +== Version 1.1.15 (December 27, 2011) + +* Fixed a bug where the push notification didn't work with +read-protected projects. (https://issues.jenkins-ci.org/browse/JENKINS-12022[JENKINS-12022]) +* Improved the handling of disabled projects in the push notification. + +== Version 1.1.14 (November 30, 2011) + +* Added support for instant commit push notifications (see also this +http://kohsuke.org/2011/12/01/polling-must-die-triggering-jenkins-builds-from-a-git-hook/[blog +post]) + +== Version 1.1.13 (November 24, 2011) + +* option to ignore submodules completely (https://issues.jenkins-ci.org/browse/JENKINS-6658[JENKINS-6658]) +* support FishEye scm browser (https://issues.jenkins-ci.org/browse/JENKINS-7849[JENKINS-7849]) +* inverse choosing strategy to select all branches except for those +specified (https://github.com/jenkinsci/git-plugin/pull/45[pull request +#45]) +* option to clone from a reference repository +* fix databinding bug (https://issues.jenkins-ci.org/browse/JENKINS-9914[JENKINS-9914]) +* action to tag a build, similar to subversion plugin feature + +== Version 1.1.12 (August 5, 2011) + +* When choosing the branch to build, Jenkins will pick up the oldest +branch to induce fairness in the scheduling. (it looks at the timestamp +of the tip of the branch.) +* Git now polls without needing a workspace (https://issues.jenkins-ci.org/browse/JENKINS-10131[JENKINS-10131]) +* Fixed the "no remote from branch name" problem (https://issues.jenkins-ci.org/browse/JENKINS-10060[JENKINS-10060]) + +== Version 1.1.11 (July 22, 2011) + +* Add support for generating links to Gitorious repositories. +(https://github.com/jenkinsci/git-plugin/pull/38[PR#38]) +* Fixed DefaultBuildChooser logic (https://issues.jenkins-ci.org/browse/JENKINS-10408[JENKINS-10408]) + +== Version 1.1.10 (July 15, 2011) + +* Merge options persist properly now. (https://issues.jenkins-ci.org/browse/JENKINS-10270[JENKINS-10270]) +* Fixed NPE in PreBuildMergeOptions when using REST API. (https://issues.jenkins-ci.org/browse/JENKINS-9843[JENKINS-9843]) +* Global config name/email handle whitespace properly. (https://issues.jenkins-ci.org/browse/JENKINS-10272[JENKINS-10272], +https://issues.jenkins-ci.org/browse/JENKINS-9566[JENKINS-9566]) +* Improved memory handling of "git whatchanged". (https://issues.jenkins-ci.org/browse/JENKINS-8365[JENKINS-8365]) +* Excluded regions should now work with multiple commit changesets. +(https://issues.jenkins-ci.org/browse/JENKINS-8342[JENKINS-8342]) +* ViewGit support added. (https://issues.jenkins-ci.org/browse/JENKINS-5163[JENKINS-5163]) +* Fixed NPE when validating remote for publisher. (https://issues.jenkins-ci.org/browse/JENKINS-9971[JENKINS-9971]) +* Tool selection persists now. (https://issues.jenkins-ci.org/browse/JENKINS-9765[JENKINS-9765]) +* Remote branch pruning now happens after fetch, to make sure all +remotes are defined. (https://issues.jenkins-ci.org/browse/JENKINS-10348[JENKINS-10348]) + +== Version 1.1.9 (May 16, 2011) + +* Don't strip off interesting stuff from branch names in token macro +(https://issues.jenkins-ci.org/browse/JENKINS-9510[JENKINS-9510]) +* Changes to serialization to support working with the MultiSCM plugin +and general cleanliness. +(https://github.com/jenkinsci/git-plugin/pull/22[PR#22]) +* Check to be sure remote actually exists in local repo before running +"git remote prune" against it. (https://issues.jenkins-ci.org/browse/JENKINS-9661[JENKINS-9661]) +* Eliminate a problem with NPEs on git config user.name/user.email usage +on upgrades. (https://issues.jenkins-ci.org/browse/JENKINS-9702[JENKINS-9702]) +* Add a check for git executable version as 1.7 or greater before using +--progress on git clone calls. (https://issues.jenkins-ci.org/browse/JENKINS-9635[JENKINS-9635]) + +== Version 1.1.8 (May 6, 2011) + +* Re-release of 1.1.7 to deal with forked version of plugin having +already released with same groupId/artifactId/version as our 1.1.7 +release, thereby breaking things. + +== Version 1.1.7 (May 4, 2011) + +* GIT_COMMIT environment variable now available in builds. +(https://issues.jenkins-ci.org/browse/JENKINS-9253[JENKINS-9253]) +* Improved wording of error message when no revision is found to build. +(https://issues.jenkins-ci.org/browse/JENKINS-9339[JENKINS-9339]) +* Added "--progress" to git clone call. (https://issues.jenkins-ci.org/browse/JENKINS-9168[JENKINS-9168]) +* Underlying error actually shown when git fetch fails. (https://issues.jenkins-ci.org/browse/JENKINS-9052[JENKINS-9052]) +* git config options for user.name and user.email now save properly. +(https://issues.jenkins-ci.org/browse/JENKINS-9071[JENKINS-9071]) +* Properly handle empty string for branch when branch is parameterized. +(https://issues.jenkins-ci.org/browse/JENKINS-8656[JENKINS-8656]) +* If no Jenkins user is found for a commit's user.name value, strip the +username from "\username@domain.com" from the user.email value and use +that instead. (https://issues.jenkins-ci.org/browse/JENKINS-9016[JENKINS-9016]) + +== Version 1.1.6 (March 8, 2011) + +* Fix for warning stacktrace if parameterized trigger plugin was not +installed. +* No longer try to generate complete history as changelog if previous +build's SHA1 no longer exists in repository. (https://issues.jenkins-ci.org/browse/JENKINS-8853[JENKINS-8853]) +* Fixed bug causing "Firstname \Lastname@domain.com" to be used as email +address for users. (https://issues.jenkins-ci.org/browse/JENKINS-7156[JENKINS-7156]) +* Passwords should now be properly used in https URLs. (https://issues.jenkins-ci.org/browse/JENKINS-3807[JENKINS-3807]) +* Exposed a few token macros + +== Version 1.1.5 (February 14, 2011) + +* Added an extension for to allow Git SHA1 of the current build to be +passed to downstream builds (so that they can act on the exact same +commit.) +* Allowed optional disabling of internal tagging (https://issues.jenkins-ci.org/browse/JENKINS-5676[JENKINS-5676]) +* If specified, use configured values for user.email and user.name +(https://issues.jenkins-ci.org/browse/JENKINS-2754[JENKINS-2754]) +* Removed obsolete/unused wipe out workspace option and defunct Gerrit +build chooser. +* Rebranded to Jenkins! + +== Version 1.1.4 (December 4, 2010) + +* For Matrix projects, push only at the end of the whole thing, not at +the configuration build (https://issues.jenkins-ci.org/browse/JENKINS-5005[JENKINS-5005]). +* Switching between browsers does not function properly (https://issues.jenkins-ci.org/browse/JENKINS-8210[JENKINS-8210]). +* Implement support for http://www.redmine.org/[Redmine] as browser. + +== Version 1.1.3 (November 8, 2010) + +* No changes except of updated version according to scm. + +== Version 1.1.2 (November 8, 2010) + +* Fixed major bug in polling (https://issues.jenkins-ci.org/browse/JENKINS-8032[JENKINS-8032]) + +== Version 1.1.1 (November 5, 2010) + +* Improved logging for failures with git fetch. +* Made sure .gitmodules is closed properly. (https://issues.jenkins-ci.org/browse/JENKINS-7659[JENKINS-7659]) +* Fixed issue with polling failing if the master has 0 executors. +(https://issues.jenkins-ci.org/browse/JENKINS-7547[JENKINS-7547]) +* Modified Git publisher to run as late as possible in the post-build +plugin order. (https://issues.jenkins-ci.org/browse/JENKINS-7877[JENKINS-7877]) +* Added optional call to "git remote prune" to prune obsolete local +branches before build. (https://issues.jenkins-ci.org/browse/JENKINS-7831[JENKINS-7831]) + +== Version 1.1 (September 21, 2010) + +* Added ability for GitPublisher to only push if build succeeds. +(https://issues.jenkins-ci.org/browse/JENKINS-7176[JENKINS-7176]) +* Fixed major bug with submodule behavior - making sure we don't try to +fetch submodules until we've finished the initial clone. (https://issues.jenkins-ci.org/browse/JENKINS-7258[JENKINS-7258]) +* "Clean after checkout" wasn't invoked when pre-build merges were +enabled. (https://issues.jenkins-ci.org/browse/JENKINS-7276[JENKINS-7276]) +* Form validation was missing for the GitPublisher tag and branch names, +and an empty value was allowed for GitPublisher target repositories, +leading to confusion. (https://issues.jenkins-ci.org/browse/JENKINS-7277[JENKINS-7277]) +* "Clean before build" will now run in submodules as well as root. +(https://issues.jenkins-ci.org/browse/JENKINS-7376[JENKINS-7376]) +* When polling, Hudson-configured environment variables were not being +used. (https://issues.jenkins-ci.org/browse/JENKINS-7411[JENKINS-7411]) +* Modifications to BuildData to deal with Hudson no longer serializing +null keys. (https://issues.jenkins-ci.org/browse/JENKINS-7446[JENKINS-7446]) +* Support for --recursive option to submodule commands. (https://issues.jenkins-ci.org/browse/JENKINS-6258[JENKINS-6258]) + +== Version 1.0.1 (August 9, 2010) + +* Fixed submodules support - was broken by https://issues.jenkins-ci.org/browse/JENKINS-6902[JENKINS-6902] fix. +(https://issues.jenkins-ci.org/browse/JENKINS-7141[JENKINS-7141]) +* Switched "Recent Changes" list for a project to count changes per +build, rather than using revision as if it were a number. (https://issues.jenkins-ci.org/browse/JENKINS-7154[JENKINS-7154]) +* Stopped putting problematic slash at end of GitWeb URL. (https://issues.jenkins-ci.org/browse/JENKINS-7020[JENKINS-7020]) + +== Version 1.0 (July 29, 2010) + +* Added support for Github as a repository browser. +* Added support for optionally putting source in a subdirectory of the +workspace (https://issues.jenkins-ci.org/browse/JENKINS-6357[JENKINS-6357]) +* If all repository fetches fail, fail the build. (https://issues.jenkins-ci.org/browse/JENKINS-6902[JENKINS-6902]) +* Improved logging of git command execution errors (https://issues.jenkins-ci.org/browse/JENKINS-6330[JENKINS-6330]) +* Basic support for excluded regions and excluded users in polling added +(https://issues.jenkins-ci.org/browse/JENKINS-4556[JENKINS-4556]) +* Support for optionally checking out to a local branch, rather than +detached HEAD (https://issues.jenkins-ci.org/browse/JENKINS-6856[JENKINS-6856]) +* Revamped GitPublisher to allow for pushing tags to remotes and pushing +to remote branches, as well as existing push of merge results. +(https://issues.jenkins-ci.org/browse/JENKINS-5371[JENKINS-5371]) + +== Version 0.9.2 (June 22, 2010) + +* Fixed major bug in BuildChooser default selection and serialization +(https://issues.jenkins-ci.org/browse/JENKINS-6827[JENKINS-6827]) + +== Version 0.9.1 (June 22, 2010) + +* Dramatic improvement in changelog generation, thanks to a switch to +use "git whatchanged" (https://issues.jenkins-ci.org/browse/JENKINS-6781[JENKINS-6781]) + +== Version 0.9 (June 17, 2010) + +* Improved support for BuildChooser as an extension point - other +plugins can now implement their own BuildChoosers and have them +automatically show up as an option in Git configuration when installed. +* Options added for wiping out the workspace before the build begins +(this option may be removed), and for using commit authors as the Hudson +changelog entry author, rather than the committers, the default +behavior. + +== Version 0.8.2 + +* Support for Gerrit plugin. +* Support for different build choosers. + +== Version 0.7.3 + +* Fixed https://issues.jenkins-ci.org/browse/JENKINS-2931[JENKINS-2931], git tag freezing job execution (jbq) +* Improve log messages (jbq) +* Use build listener to report messages when pushing tags to origin +(jbq) +* Fixed https://issues.jenkins-ci.org/browse/JENKINS-2762[JENKINS-2762], fail to clone a repository on Windows (jbq) + +== Version 0.5 + +* Fix git plugin which was very broken when running on a remote server +(magnayn) +* Fix NPE in GitChangeLogParser upon project's first build (jbq) +* Change workspace to a FilePath in GitAPI (jbq) +* Use git rev-list once instead of invoking git rev-parse indefinitely +to find last build, see https://issues.jenkins-ci.org/browse/JENKINS-2469[JENKINS-2469]: GIT plugin very slow (jbq) +* Handle null-value of the repositories field to ensure +backwards-compatibility with version 0.3, + +ie when the project configuration is missing the XML +element (jbq) +* Improve error handling in revParse() (jbq) +* Fix handling of the "branch" configuration parameter (jbq) +* Improve tag handling, use show-ref instead of rev-parse to resolve the +tag reference (jbq) +* Fix https://issues.jenkins-ci.org/browse/JENKINS-2675[JENKINS-2675], Git fails on remote slaves (jbq) + +== Version 0.4 (never released) + +* Allow multiple GIT repositories to be specified (magnayn) +* Allow submodule configurations to be generated on the fly (magnayn) +* Avoid infinite loops when git doesn't contains tags (david_calavera) +* Don't do a log of the entire branch if it's never been built (magnayn) + +== Version 0.3 + +* Add support for pre-build branch merges + +== Version 0.2 + +* Improve handling of git repositories (use local tags to identify up to +date versions rather than the wc HEAD) +* Don't have to specify a branch, in which case all branches are +examined for changes and built +* Includes a publisher which can be used to push build success/failure +tags back up to the upstream repository + +== Version 0.1 + +* Initial Release +* Allow extension to require workspace for polling (https://issues.jenkins-ci.org/browse/JENKINS-19001[JENKINS-19001]) diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000000..4a55796fe5 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,64 @@ += Contributing to the Git Plugin + +The git plugin implements the https://plugins.jenkins.io/scm-api[Jenkins SCM API]. +Refer to the SCM API documentation for https://github.com/jenkinsci/scm-api-plugin/blob/master/docs/implementation.adoc#naming-your-plugin[plugin naming conventions] +and for the https://github.com/jenkinsci/scm-api-plugin/blob/master/CONTRIBUTING.md#add-to-core-or-create-extension-plugin[preferred locations of new functionality]. + +Plugin source code is hosted on https://github.com/jenkinsci/git-plugin[GitHub]. +New feature proposals and bug fix proposals should be submitted as https://help.github.com/articles/creating-a-pull-request[GitHub pull requests]. +Your pull request will be evaluated by the https://ci.jenkins.io/job/Plugins/job/git-plugin/[Jenkins job]. + +Before submitting your change, please assure that you've added tests which verify your change. +There have been many developers involved in the git plugin and there are many, many users who depend on the git plugin. +Tests help us assure that we're delivering a reliable plugin, and that we've communicated our intent to other developers as executable descriptions of plugin behavior. + +Code coverage reporting is available as a maven target. +Please try to improve code coverage with tests when you submit. + +* `mvn -P enable-jacoco clean install jacoco:report` to report code coverage + +Please don't introduce new spotbugs output. + +* `mvn spotbugs:check` to analyze project using https://spotbugs.github.io/[Spotbugs]. +* `mvn spotbugs:gui` to review Spotbugs report using GUI + +Code formatting in the git plugin varies between files. +Try to maintain reasonable consistency with the existing files where feasible. +Please don't perform wholesale reformatting of a file without discussing with the current maintainers. +New code should follow the https://github.com/jenkinsci/scm-api-plugin/blob/master/CONTRIBUTING.md#code-style-guidelines[SCM API code style guidelines]. + +[[pull-request-review]] +== Reviewing Pull Requests + +Maintainers triage pull requests by reviewing them and by assigning labels. +Release drafter uses the labels to automate link:https://github.com/jenkinsci/git-plugin/releases[release notes]. +link:Priorities.adoc#git-plugin-development-priorities[Prioritization] uses the labels to group relevant pull requests. + +Others are encouraged to review pull requests, test pull request builds, and report their results in the pull request. +Refer to the link:Priorities.adoc#priorities[maintainer's priority list] for topics the plugin maintainers are considering. + +=== Testing a Pull Request Build + +Pull request builds merge the most recent changes from their target branch with the change proposed in the pull request. +They can be downloaded from ci.jenkins.io and used to test the pull request. +Steps to test a pull request build are: + +. *Find the pull request on link:https://github.com/jenkinsci/git-plugin/pulls[GitHub]* - for example, link:https://github.com/jenkinsci/git-plugin/pull/676[pull request 676] +. *Find the link:https://ci.jenkins.io/job/Plugins/job/git-plugin/view/change-requests/[ci.jenkins.io] artifacts for that pull request* from the link to the link:https://ci.jenkins.io/job/Plugins/job/git-plugin/job/PR-676/lastSuccessfulBuild/[artifacts in the Jenkins job] in "*Show all checks*" +. *Download the `hpi` file* (like `git-4.0.1-rc3444.b3d767e3d46a.hpi`) to your computer +. *Upload the `hpi` file* to Jenkins from the Jenkins Plugin Manager +. *Restart your Jenkins* and you're ready to test + +[[bug-triage]] +== Reviewing Bug Reports + +Git plugin bug reports are assigned to one of the maintainers by default. +link:https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open)%20AND%20component%20%3D%20git-plugin%20and%20assignee%20in%20(rsandell%2Cmarkewaite%2Cfcojfernandez)[Open bug reports] assigned to a maintainer are assumed to have not been reviewed. +When a maintainer completes review of an issue, they include a comment on the bug report and set the 'Assignee' to 'Unassigned'. + +Others are welcome to review bug reports, comment on the results of the review, and set the 'Assignee' to 'Unassigned'. +Typical bug review tasks include: + +* Review summary and description +* Attempt to duplicate the issue +* Add a comment with results of the attempt to duplicate the issue diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000..c8cae0ec98 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,63 @@ +#!groovy + +// Test plugin compatibility to recommended configurations +// Allow failing tests to retry execution +subsetConfiguration = [ [ jdk: '8', platform: 'windows', jenkins: null ], + [ jdk: '8', platform: 'linux', jenkins: '2.164.1', javaLevel: '8' ], + [ jdk: '11', platform: 'linux', jenkins: '2.164.1', javaLevel: '8' ] + ] + +buildPlugin(configurations: subsetConfiguration, failFast: false) + +def branches = [:] + +branches["ATH"] = { + node("docker && highmem") { + def checkoutGit + stage("ATH: Checkout") { + checkoutGit = pwd(tmp:true) + "/athgit" + dir(checkoutGit) { + checkout scm + infra.runMaven(["clean", "package", "-DskipTests"]) + // Include experimental git-client in target dir for ATH + // This Git plugin requires experimental git-client + infra.runMaven(["dependency:copy", "-Dartifact=org.jenkins-ci.plugins:git-client:3.0.0-beta3:hpi", "-DoutputDirectory=target", "-Dmdep.stripVersion=true"]) + dir("target") { + stash name: "localPlugins", includes: "*.hpi" + } + } + } + def metadataPath = checkoutGit + "/essentials.yml" + stage("Run ATH") { + def athFolder=pwd(tmp:true) + "/ath" + dir(athFolder) { + runATH metadataFile: metadataPath + } + } + } +} +branches["PCT"] = { + node("docker && highmem") { + def metadataPath + env.RUN_PCT_LOCAL_PLUGIN_SOURCES_STASH_NAME = "localPluginsPCT" + stage("PCT: Checkout") { + def checkoutGit = pwd(tmp:true) + "/pctgit" + dir(checkoutGit) { + dir("git") { + checkout scm + } + stash name: "localPluginsPCT", useDefaultExcludes: false + } + metadataPath = checkoutGit + "/git/essentials.yml" + } + stage("Run PCT") { + def pctFolder = pwd(tmp:true) + "/pct" + dir(pctFolder) { + runPCT metadataFile: metadataPath + } + } + } +} + +// Intentionally disabled until tests are more reliable +// parallel branches diff --git a/Priorities.adoc b/Priorities.adoc new file mode 100644 index 0000000000..29d874d0a0 --- /dev/null +++ b/Priorities.adoc @@ -0,0 +1,31 @@ +[[git-plugin-development-priorities]] += Git Plugin Development Priorities + +[[introduction]] +== Introduction + +The maintainers of this plugin use this file to list the work areas which are receiving more attention from them. +Higher priority items will generally receive more attention from maintainers than lower priority items. +Others are encouraged to help with the plugin in areas of their own interest. +When others provide additional help on an item, it tends to raise the priority of the item. + +== Priorities + +. link:CONTRIBUTING.adoc#bug-triage[Bug report triage] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3AShortTerm[Short term improvements] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3ACaching[Caching and performance improvements] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3AnotifyCommit[notifyCommit and webhook improvements] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3AChangelog+[Changelog improvements] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3ASubmodules[Submodule improvements] +. link:https://github.com/jenkinsci/git-plugin/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+label%3ABuildData[BuildData memory bloat] + +[[bug-reports]] +== Bug Reports + +Report issues and enhancements in the https://issues.jenkins-ci.org[Jenkins issue tracker]. +link:CONTRIBUTING.adoc#bug-triage[Review bug reports] and provide comments to submitters. + +[[contributing-to-the-plugin]] +== Contributing to the Plugin + +Refer to link:CONTRIBUTING.adoc#contributing-to-the-git-plugin[contributing to the plugin] for contribution guidelines. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000000..9f857b7bbb --- /dev/null +++ b/README.adoc @@ -0,0 +1,960 @@ +[[git-plugin]] += Git Plugin +:toc: macro +:toc-title: + +link:https://ci.jenkins.io/job/Plugins/job/git-plugin/job/master/[image:https://ci.jenkins.io/job/Plugins/job/git-plugin/job/master/badge/icon[Build]] +link:https://github.com/jenkinsci/git-plugin/graphs/contributors[image:https://img.shields.io/github/contributors/jenkinsci/git-plugin.svg?color=blue[Contributors]] +link:https://plugins.jenkins.io/git[image:https://img.shields.io/jenkins/plugin/i/git.svg?color=blue&label=installations[Jenkins Plugin Installs]] +link:https://github.com/jenkinsci/git-plugin/releases/latest[image:https://img.shields.io/github/release/jenkinsci/git-plugin.svg?label=changelog[GitHub release]] +link:https://gitter.im/jenkinsci/git-plugin[image:https://badges.gitter.im/jenkinsci/git-plugin.svg[Gitter]] + +[[introduction]] +== Introduction + +[.float-group] +-- +[.text-center] +image:https://jenkins.io/images/logos/jenkins/jenkins.png[Jenkins logo,height=192,role=center,float=right] +image:images/signe-1923369_640.png[plus,height=64,float=right] +image:https://git-scm.com/images/logos/downloads/Git-Logo-2Color.png[Git logo,height=128,float=right] +-- + +The git plugin provides fundamental git operations for Jenkins projects. +It can poll, fetch, checkout, branch, list, merge, tag, and push repositories. + +toc::[] + +[[changelog]] +== Changelog in https://github.com/jenkinsci/git-plugin/releases[GitHub Releases] + +Release notes are recorded in https://github.com/jenkinsci/git-plugin/releases[GitHub Releases] since July 1, 2019 (git plugin 3.10.1 and later). +Prior release notes are recorded in the git plugin repository link:CHANGELOG.adoc#changelog-moved-to-github-releases[change log]. + +[[configuration]] +== Configuration + +[[using-repositories]] +=== Repositories + +The git plugin fetches commits from one or more remote repositories and performs a checkout in the agent workspace. +Repositories and their related information include: + +Repository URL:: + + The URL of the remote repository. + The git plugin passes the remote repository URL to the git implementation (command line or JGit). + Valid repository URL's include `https`, `ssh`, `scp`, `git`, `local file`, and other forms. + Valid repository URL forms are described in the link:https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_protocols[git documentation]. + +Credentials:: + + Credentials are defined using the link:https://plugins.jenkins.io/credentials[Jenkins credentials plugin]. + They are selected from a drop-down list and their identifier is stored in the job definition. + Refer to <> for more details on supported credential types. + +Name:: + + Git uses a shortname to simplify user references to the URL of the remote repository. + The default shortname is `origin`. + Other values may be assigned and then used throughout the job definition to refer to the remote repository. + +Refspec:: + + A refspec maps remote branches to local references. + It defines the branches and tags which will be fetched from the remote repository into the agent workspace. ++ +A refspec defines the remote references that will be retrieved and how they map to local references. +If left blank, it will default to the normal `git fetch` behavior and will retrieve all branches. +This default behavior is sufficient for most cases. ++ +The default refspec is `+refs/heads/*:refs/remotes/REPOSITORYNAME/` where REPOSITORYNAME is the value you specify in the above repository "Name" field. +The default refspec retrieves all branches. +If a checkout only needs one branch, then a more restrictive refspec can reduce the data transfer from the remote repository to the agent workspace. +For example, `+refs/heads/master:refs/remotes/origin/master` will retrieve only the master branch and nothing else. ++ +The refspec can be used with the <> option in the <> to limit the number of remote branches mapped to local references. +If "honor refspec on initial clone" is not enabled, then a default refspec for its initial fetch. +This maintains compatibility with previous behavior and allows the job definition to decide if the refspec should be honored on initial clone. ++ +Multiple refspecs can be entered by separating them with a space character. +The refspec value `+refs/heads/master:refs/remotes/origin/master +refs/heads/develop:refs/remotes/origin/develop` retrieves the master branch and the develop branch and nothing else. ++ +Refer to the link:https://git-scm.com/book/en/v2/Git-Internals-The-Refspec[git refspec documentation] for more refspec details. + +[[using-credentials]] +=== Using Credentials + +The git plugin supports username / password credentials and private key credentials provided by the +https://plugins.jenkins.io/credentials[Jenkins credentials plugin]. +It does not support other credential types like secret text, secret file, or certificates. +Select credentials from the job definition drop down menu or enter their identifiers in Pipeline job definitions. + +[[enabling-jgit]] +=== Enabling JGit + +See the https://plugins.jenkins.io/git-client[git client plugin documentation] for instructions to enable JGit. +JGit becomes available throughout Jenkins once it has been enabled. + +[[repository-browser]] +=== Repository Browser + +A Repository Browser adds links in "changes" views within Jenkins to an external system for browsing the details of those changes. +The "Auto" selection attempts to infer the repository browser from the "Repository URL" and can detect Cloud versions of GitHub, Bitbucket and GitLab. + +Repository browsers include: + +[[assemblaweb-repository-browser]] +==== AssemblaWeb + +Repository browser for git repositories hosted by link:https://www.assembla.com/home[Assembla]. +Options include: + +[[assembla-git-url]] +Assembla Git URL:: + + Root URL serving this Assembla repository. + For example, `\https://app.assembla.com/spaces/git-plugin/git/source` + +[[fisheye-repository-browser]] +==== FishEye + +Repository browser for git repositories hosted by link:https://www.atlassian.com/software/fisheye[Atlassian Fisheye]. +Options include: + +[[fisheye-url]] +URL:: + + Root URL serving this FishEye repository. + For example, `\https://fisheye.example.com/browser/my-project` + +[[gitlab-com-repository-browser]] +==== GitLab + +Repository browser for git repositories hosted on link:https://gitlab.com[GitLab.com]. +No options can be configured for this repository browser. +Users hosting their own instances of GitLab should use the <> browser instead. + +[[gitea-repository-browser]] +==== Gitea + +Repository browser for git repositories hosted by link:https://gitea.io/[Gitea]. +Options include: + +[[gitea-url]] +Repository URL:: + + Root URL serving this gitea repository. + For example, `\https://gitea.example.com/username/project-name` + +[[kiln-repository-browser]] +==== Kiln + +Repository browser for git repositories hosted by link:http://www.fogbugz.com/version-control[Kiln]. +Options include: + +[[kiln-url]] +URL:: + + Root URL serving this Kiln repository. + For example, `\https://kiln.example.com/username/my-project` + +[[visual-studio-team-services-repository-browser]] +==== Microsoft Team Foundation Server/Visual Studio Team Services + +Repository browser for git repositories hosted by link:https://azure.microsoft.com/en-us/solutions/devops/[Azure DevOps]. +Options include: + +[[visual-studio-repository-url-or-name]] +URL or name:: + + Root URL serving this Azure DevOps repository. + For example, `\https://example.visualstudio.com/_git/my-project.` + +[[bitbucketweb-repository-browser]] +==== bitbucketweb + +Repository browser for git repositories hosted by link:https://bitbucket.org/[Bitbucket]. +Options include: + +[[bitbucketweb-url]] +URL:: + + Root URL serving this Bitbucket repository. + For example, `\https://bitbucket.example.com/username/my-project` + +[[cgit-repository-browser]] +==== cgit + +Repository browser for git repositories hosted by link:https://git.zx2c4.com/cgit/[cgit]. +Options include: + +[[cgit-url]] +URL:: + + Root URL serving this cgit repository. + For example, `\https://git.zx2c4.com/cgit/` + +[[gitblit-repository-browser]] +==== gitblit + +[[gitblit-url]] +GitBlit root url:: + + Root URL serving this GitBlit repository. + For example, `\https://gitblit.example.com/cgit/` + +[[gitblit-project-name]] +Project name in GitBlit:: + + Name of the GitBlit project. + For example, `my-project` + +[[githubweb-repository-browser]] +==== githubweb + +Repository browser for git repositories hosted by link:https://github.com//[GitHub]. +Options include: + +[[githubweb-url]] +GitHub root url:: + + Root URL serving this GitHub repository. + For example, `\https://github.example.com/username/my-project` + +[[gitiles-repository-browser]] +==== gitiles + +Repository browser for git repositories hosted by link:https://gerrit.googlesource.com/gitiles/[Gitiles]. +Options include: + +[[githubweb-url]] +gitiles root url:: + + Root URL serving this Gitiles repository. + For example, `\https://gerrit.googlesource.com/gitiles/` + +[[gitlab-self-hosted-repository-browser]] +==== gitlab + +Repository browser for git repositories hosted by link:https://gitlab.com/[GitLab]. +Options include: + +[[gitlab-url]] +URL:: + + Root URL serving this GitLab repository. + For example, `\https://gitlab.example.com/username/my-project` + +[[gitlab-version]] +Version:: + + Major and minor version of GitLab you use, such as 12.6. + If you don't specify a version, a modern version of GitLab (>= 8.0) is assumed. + For example, `12.6` + +[[gitlist-repository-browser]] +==== gitlist + +Repository browser for git repositories hosted by link:https://gitlist.org/[GitList]. +Options include: + +[[gitlist-url]] +URL:: + + Root URL serving this GitList repository. + For example, `\https://gitlist.example.com/username/my-project` + +[[gitoriousweb-repository-browser]] +==== gitoriousweb + +Gitorious was acquired in 2015. +This browser is *deprecated*. + +[[gitoriousweb-url]] +URL:: + + Root URL serving this Gitorious repository. + For example, `\https://gitorious.org/username/my-project` + +[[gitweb-repository-browser]] +==== gitweb + +Repository browser for git repositories hosted by link:https://git-scm.com/docs/gitweb[GitWeb]. +Options include: + +[[gitweb-url]] +URL:: + + Root URL serving this GitWeb repository. + For example, `\https://gitweb.example.com/username/my-project` + +[[gogs-repository-browser]] +==== gogs + +Repository browser for git repositories hosted by link:https://gogs.io/[Gogs]. +Options include: + +[[gogs-url]] +URL:: + + Root URL serving this Gogs repository. + For example, `\https://gogs.example.com/username/my-project` + +[[phabricator-repository-browser]] +==== phabricator + +Repository browser for git repositories hosted by link:https://www.phacility.com/phabricator/[Phacility Phabricator]. +Options include: + +[[phabricator-url]] +URL:: + + Root URL serving this Phabricator repository. + For example, `\https://phabricator.example.com/` + +[[phabricator-repository-name]] +Repository name in Phab:: + + Name of the Phabricator repository. + For example, `my-project` + +[[redmineweb-repository-browser]] +==== redmineweb + +Repository browser for git repositories hosted by link:https://www.redmine.org/[Redmine]. +Options include: + +[[redmineweb-url]] +URL:: + + Root URL serving this Redmine repository. + For example, `\https://redmine.example.com/username/projects/my-project/repository` + +[[rhodecode-repository-browser]] +==== rhodecode + +Repository browser for git repositories hosted by link:https://thodecode.com/[RhodeCode]. +Options include: + +[[rhodecode-url]] +URL:: + + Root URL serving this RhodeCode repository. + For example, `\https://rhodecode.example.com/username/my-project` + +[[stash-repository-browser]] +==== stash + +Stash is now called *BitBucket Server*. +Repository browser for git repositories hosted by link:https://www.atlassian.com/software/bitbucket[BitBucket Server]. +Options include: + +stash +[[stash-url]] +URL:: + + Root URL serving this Stash repository. + For example, `\https://stash.example.com/username/my-project` + +[[viewgit-repository-browser]] +==== viewgit + +Repository browser for git repositories hosted by link:https://www.openhub.net/p/viewgit[viewgit]. +Options include: + +[[viewgit-root-url]] +ViewGit root url:: + + Root URL serving this ViewGit repository. + For example, `\https://viewgit.example.com/` + +[[viewgit-project-name]] +Project Name in ViewGit:: + + ViewGit project name. + For example, `my-project` + +[[extensions]] +== Extensions + +Extensions add new behavior or modify existing plugin behavior for different uses. +Extensions help users more precisely tune the plugin to meet their needs. + +Extensions include: + +- <> +- <> +- <> +- <> +- <> +- <> +- <> + +[[clone-extensions]] +=== Clone Extensions + +[[advanced-clone-behaviours]] +==== Advanced clone behaviours + +Advanced clone behaviors modify the `link:https://git-scm.com/docs/git-clone[git clone]` and `link:https://git-scm.com/docs/git-fetch[git fetch]` commands. +They control: + +* breadth of history retrieval (refspecs) +* depth of history retrieval (shallow clone) +* disc space use (reference repositories) +* duration of the command (timeout) +* tag retrieval + +Advanced clone behaviors include: + +[[honor-refspec-on-initial-clone]] +Honor refspec on initial clone:: + + Perform initial clone using the refspec defined for the repository. + This can save time, data transfer and disk space when you only need to access the references specified by the refspec. + If this is not enabled, then the plugin default refspec includes **all** remote branches. + +Shallow clone:: + + Perform a shallow clone by requesting a limited number of commits from the tip of the requested branch(es). + Git will not download the complete history of the project. + This can save time and disk space when you just want to access the latest version of a repository. + +Shallow clone depth:: + + Set shallow clone depth to the specified number of commits. + Git will only download `depth` commits from the remote repository, saving time and disk space. + +Path of the reference repo to use during clone:: + + Specify a folder containing a repository that will be used by git as a reference during clone operations. + This option will be ignored if the folder is not available on the agent. + +Timeout (in minutes) for clone and fetch operations:: + + Specify a timeout (in minutes) for clone and fetch operations. + +Fetch tags:: + + Deselect this to perform a clone without tags, saving time and disk space when you want to access only what is specified by the refspec, without considering any repository tags. + +[[checkout-extensions]] +=== Checkout Extensions + +[[advanced-checkout-behaviors]] +==== Advanced checkout behaviors + +Advanced checkout behaviors modify the `link:https://git-scm.com/docs/git-checkout[git checkout]` command. +Advanced checkout behaviors include + +Timeout (in minutes) for checkout operation:: + + Specify a timeout (in minutes) for checkout. + The checkout is stopped if the timeout is exceeded. + Checkout timeout is usually only required with slow file systems or large repositories. + +[[advanced-sub-modules-behaviours]] +==== Advanced sub-modules behaviours + +Advanced sub-modules behaviors modify the `link:https://git-scm.com/docs/git-submodule[git submodule]` commands. +They control: + +* depth of history retrieval (shallow clone) +* disc space use (reference repositories) +* credential use +* duration of the command (timeout) +* concurrent threads used to fetch submodules + +Advanced sub-modules include: + +Disable submodules processing:: + + Ignore submodules in the repository. + +Recursively update submodules:: + + Retrieve all submodules recursively. Without this option, submodules + which contain other submodules will ignore the contained submodules. + +Update tracking submodules to tip of branch:: + + Retrieve the tip of the configured branch in .gitmodules. + +Use credentials from default remote of parent repository:: + + Use credentials from the default remote of the parent project. + Submodule updates do not use credentials by default. + Enabling this extension will provide the parent repository credentials to each of the submodule repositories. + Submodule credentials require that the submodule repository must accept the same credentials as the parent project. + If the parent project is cloned with https, then the authenticated submodule references must use https as well. + If the parent project is cloned with ssh, then the authenticated submodule references must use ssh as well. + +Shallow clone:: + + Perform shallow clone of submodules. + Git will not download the complete history of the project, saving time and disk space. + +Shallow clone depth:: + + Set shallow clone depth for submodules. + Git will only download recent history of the project, saving time and disk space. + +Path of the reference repo to use during submodule update:: + + Folder containing a repository that will be used by git as a reference during submodule clone operations. + This option will be ignored if the folder is not available on the agent running the build. + A reference repository may contain multiple subprojects. + See the combining repositories section for more details. + +Timeout (in minutes) for submodule operations:: + + Specify a timeout (in minutes) for submodules operations. + This option overrides the default timeout. + +Number of threads to use when updating submodules:: + + Number of parallel processes to be used when updating submodules. + Default is to use a single thread for submodule updates + +[[checkout-to-a-sub-directory]] +==== Checkout to a sub-directory + +Checkout to a subdirectory of the workspace instead of using the workspace root. + +This extension should **not** be used in Jenkins Pipeline (either declarative or scripted). +Jenkins Pipeline already provides standard techniques for checkout to a subdirectory. +Use `ws` and `dir` in Jenkins Pipeline rather than this extension. + +Local subdirectory for repo:: + + Name of the local directory (relative to the workspace root) for the git repository checkout. + If left empty, the workspace root itself will be used. + +[[checkout-to-specific-local-branch]] +==== Checkout to specific local branch + +Branch name:: + + If given, checkout the revision to build as HEAD on the named branch. + If value is an empty string or "**", then the branch name is computed from the remote branch without the origin. + In that case, a remote branch 'origin/master' will be checked out to a local branch named 'master', and a remote branch 'origin/develop/new-feature' will be checked out to a local branch named 'develop/new-feature'. + +[[wipe-out-repository-and-force-clone]] +==== Wipe out repository and force clone + +Delete the contents of the workspace before build and before checkout. +Deletes the git repository inside the workspace and will force a full clone. + +[[clean-after-checkout]] +==== Clean after checkout + +Clean the workspace *after* every checkout by deleting all untracked files and directories, including those which are specified in `.gitignore`. +Resets all tracked files to their versioned state. +Ensures that the workspace is in the same state as if clone and checkout were performed in a new workspace. +Reduces the risk that current build will be affected by files generated by prior builds. +Does not remove files outside the workspace (like temporary files or cache files). +Does not remove files in the `.git` repository of the workspace. + +Delete untracked nested repositories:: + + Remove subdirectories which contain `.git` subdirectories if this option is enabled. + This is implemented in command line git as `git clean -xffd`. + Refer to the link:https://git-scm.com/docs/git-clean[git clean manual page] for more information. + +[[clean-before-checkout]] +==== Clean before checkout + +Clean the workspace *before* every checkout by deleting all untracked files and directories, including those which are specified in .gitignore. +Resets all tracked files to their versioned state. +Ensures that the workspace is in the same state as if cloned and checkout were performed in a new workspace. +Reduces the risk that current build will be affected by files generated by prior builds. +Does not remove files outside the workspace (like temporary files or cache files). +Does not remove files in the `.git` repository of the workspace. + +Delete untracked nested repositories:: + + Remove subdirectories which contain `.git` subdirectories if this option is enabled. + This is implemented in command line git as `git clean -xffd`. + Refer to the link:https://git-scm.com/docs/git-clean[git clean manual page] for more information. + +[[git-lfs-pull-after-checkout]] +==== Git LFS pull after checkout + +Enable https://git-lfs.github.com/[git large file support] for the workspace by pulling large files after the checkout completes. +Requires that the master and each agent performing an LFS checkout have installed `git lfs`. + +[[changelog-extensions]] +=== Changelog Extensions + +The plugin can calculate the source code differences between two builds. +Changelog extensions adapt the changelog calculations for different cases. + +[[calculate-changelog-against-a-specific-branch]] +==== Calculate changelog against a specific branch + +'Calculate changelog against a specific branch' uses the specified branch to compute the changelog instead of computing it based on the previous build. +This extension can be useful for computing changes related to a known base branch, especially in environments which do not have the concept of a "pull request". + +Name of repository:: + + Name of the repository, such as 'origin', that contains the branch. + +Name of branch:: + + Name of the branch used for the changelog calculation within the named repository. + +[[use-commit-author-in-changelog]] +==== Use commit author in changelog + +The default behavior is to use the Git commit's "Committer" value in build changesets. +If this option is selected, the git commit's "Author" value is used instead. + +[[tagging-extensions]] +=== Tagging Extensions + +[[create-a-tag-for-every-build]] +==== Create a tag for every build + +Create a tag in the workspace for every build to unambiguously mark the commit that was built. +You can combine this with Git publisher to push the tags to the remote repository. + +[[build-initiation-extensions]] +=== Build initiation extensions + +The git plugin can start builds based on many different conditions. + +[[dont-trigger-a-build-on-commit-notifications]] +==== Don't trigger a build on commit notifications + +If checked, this repository will be ignored when the notifyCommit URL is accessed whether the repository matches or not. + +[[force-polling-using-workspace]] +==== Force polling using workspace + +The git plugin polls remotely using `ls-remote` when configured with a single branch (no wildcards!). +When this extension is enabled, the polling is performed from a cloned copy of the workspace instead of using `ls-remote`. + +If this option is selected, polling will use a workspace instead of using `ls-remote`. + +[[merge-extensions]] +=== Merge Extensions + +[[merge-before-build]] +==== Merge before build + +These options allow you to perform a merge to a particular branch before building. +For example, you could specify an integration branch to be built, and to merge to master. +In this scenario, on every change of integration, Jenkins will perform a merge with the master branch, and try to perform a build if the merge is successful. +It then may push the merge back to the remote repository if the <> is selected. + +Name of repository:: + + Name of the repository, such as origin, that contains the branch. If + left blank, it'll default to the name of the first repository + configured. + +Branch to merge to:: + + The name of the branch within the named repository to merge to, such as + master. + +Merge strategy:: + + Merge strategy selection. Choices include: + +* default +* resolve +* recursive +* octopus +* ours +* subtree +* recursive_theirs + +Fast-forward mode:: + +* `--ff`: fast-forward which gracefully falls back to a merge commit when required +* `-ff-only`: fast-forward without any fallback +* `--no-ff`: merge commit always, even if a fast-forward would have been allowed + +[[custom-user-name-e-mail-address]] +==== Custom user name/e-mail address + +user.name:: + + Defines the user name value which git will assign to new commits made in + the workspace. If given, git config user.name [this] is called before + builds. This overrides values from the global settings. + +user.email:: + + Defines the user email value which git will assign to new commits made + in the workspace. If given, git config user.email [this] is called + before builds. This overrides whatever is in the global settings. + +[[polling-ignores-commits-from-certain-users]] +==== Polling ignores commits from certain users + +These options allow you to perform a merge to a particular branch before building. +For example, you could specify an integration branch to be built, and to merge to master. +In this scenario, on every change of integration, Jenkins will perform a merge with the master branch, and try to perform a build if the merge is successful. +It then may push the merge back to the remote repository if the Git Push post-build action is selected. + +Excluded Users:: + + If set and Jenkins is configured to poll for changes, Jenkins will ignore any revisions committed by users in this list when determining if a build should be triggered. + This can be used to exclude commits done by the build itself from triggering another build, assuming the build server commits the change with a distinct SCM user. + Using this behavior prevents the faster `git ls-remote` polling mechanism. + It forces polling to require a workspace, as if you had selected the <> extension. + + Each exclusion uses literal pattern matching, and must be separated by a new line. + +[[polling-ignores-commits-in-certain-paths]] +==== Polling ignores commits in certain paths + +If set and Jenkins is configured to poll for changes, Jenkins will pay attention to included and/or excluded files and/or folders when determining if a build needs to be triggered. + +Using this behavior will preclude the faster remote polling mechanism, forcing polling to require a workspace thus sometimes triggering unwanted builds, as if you had selected the <> extension as well. +This can be used to exclude commits done by the build itself from triggering another build, assuming the build server commits the change with a distinct SCM user. +Using this behavior will preclude the faster git ls-remote polling mechanism, forcing polling to require a workspace, as if you had selected the <> extension as well. + +Included Regions:: + + Each inclusion uses java regular expression pattern matching, and must be separated by a new line. + An empty list implies that everything is included. + +Excluded Regions:: + + Each exclusion uses java regular expression pattern matching, and must be separated by a new line. + An empty list excludes nothing. + +[[polling-ignores-commits-with-certain-messages]] +==== Polling ignores commits with certain messages + +Excluded Messages:: + + If set and Jenkins is set to poll for changes, Jenkins will ignore any revisions committed with message matched to the regular expression pattern when determining if a build needs to be triggered. + This can be used to exclude commits done by the build itself from triggering another build, assuming the build server commits the change with a distinct message. + You can create more complex patterns using embedded flag expressions. + +[[prune-stale-remote-tracking-branches]] +==== Prune stale remote tracking branches + +Runs `link:https://git-scm.com/docs/git-remote[git remote prune]` for each remote to prune obsolete local branches. + +[[sparse-checkout-paths]] +==== Sparse Checkout paths + +Specify the paths that you'd like to sparse checkout. +This may be used for saving space (Think about a reference repository). +Be sure to use a recent version of Git, at least above 1.7.10. + +Multiple sparse checkout path values can be added to a single job. + +Path:: + + File or directory to be included in the checkout + +[[strategy-for-choosing-what-to-build]] +==== Strategy for choosing what to build + +When you are interested in using a job to build multiple branches, you can choose how Jenkins chooses the branches to build and the order they should be built. + +This extension point in Jenkins is used by many other plugins to control the job as it builds specific commits. +When you activate those plugins, you may see them installing a custom build strategy. + +Ancestry:: + +Maximum Age of Commit:: + + The maximum age of a commit (in days) for it to be built. + This uses the GIT_COMMITTER_DATE, not GIT_AUTHOR_DATE + +Commit in Ancestry:: + + If an ancestor commit (SHA-1) is provided, only branches with this commit in their history will be built. + +Default:: + + Build all the branches that match the branch name pattern. + +Inverse:: + + Build all branches except for those which match the branch specifiers configure above. + This is useful, for example, when you have jobs building your master and various release branches and you want a second job which builds all new feature branches. + For example, branches which do not match these patterns without redundantly building master and the release branches again each time they change. + +[[deprecated-extensions]] +=== Deprecated Extensions + +[[custom-scm-name---deprecated]] +==== Custom SCM name - *Deprecated* + +Unique name for this SCM. +Was needed when using Git within the Multi SCM plugin. +Pipeline is the robust and feature-rich way to checkout from multiple repositories in a single job. + +[[environment-variables]] +== Environment Variables + +The git plugin assigns values to environment variables in several contexts. +Environment variables are assigned in Freestyle, Pipeline, Multibranch Pipeline, and Organization Folder projects. + +=== Branch Variables + +GIT_BRANCH:: Name of branch being built including remote name, as in `origin/master` +GIT_LOCAL_BRANCH:: Name of branch being built without remote name, as in `master` + +=== Commit Variables + +GIT_COMMIT:: SHA-1 of the commit used in this build +GIT_PREVIOUS_COMMIT:: SHA-1 of the commit used in the preceding build of this project +GIT_PREVIOUS_SUCCESSFUL_COMMIT:: SHA-1 of the commit used in the most recent successful build of this project + +=== System Configuration Variables + +GIT_URL:: Remote URL of the first git repository in this workspace +GIT_URL_n:: Remote URL of the additional git repositories in this workspace (if any) +GIT_AUTHOR_EMAIL:: Author e-mail address that will be used for **new commits in this workspace** +GIT_AUTHOR_NAME:: Author name that will be used for **new commits in this workspace** +GIT_COMMITTER_EMAIL:: Committer e-mail address that will be used for **new commits in this workspace*** +GIT_COMMITTER_NAME:: Committer name that will be used for **new commits in this workspace** + +[[properties]] +== Properties + +Some git plugin settings can only be controlled from command line properties set at Jenkins startup. + +=== Default timeout + +The default git timeout value (in minutes) can be overridden by the `org.jenkinsci.plugins.gitclient.Git.timeOut` property (see https://issues.jenkins-ci.org/browse/JENKINS-11286[JENKINS-11286])). +The property should be set on the master and on all agents to have effect (see https://issues.jenkins-ci.org/browse/JENKINS-22547[JENKINS-22547]). + +[[git-publisher]] +== Git Publisher + +The Jenkins git plugin provides a "git publisher" as a post-build action. +The git publisher can push commits or tags from the workspace of a Freestyle project to the remote repository. + +The git publisher is **only available** for Freestyle projects. +It is **not available** for Pipeline, Multibranch Pipeline, Organization Folder, or any other job type other than Freestyle. + +=== Git Publisher Options + +The git publisher behaviors are controlled by options that can be configured as part of the Jenkins job. +Options include; + +Push Only If Build Succeeds:: + + Only push changes from the workspace to the remote repository if the build succeeds. + If the build status is unstable, failed, or canceled, the changes from the workspace will not be pushed. + +[[publisher-push-merge-results]] +Merge Results:: + + If pre-build merging is configured through one of the <>, then enabling this checkbox will push the merge to the remote repository. + +[[publisher-tag-force-push]] +Force Push:: + + Git refuses to replace a remote commit with a different commit. + This prevents accidental overwrite of new commits on the remote repository. + However, there may be times when overwriting commits on the remote repository is acceptable and even desired. + If the commits from the local workspace should overwrite commits on the remote repository, enable this option. + It will request that the remote repository destroy history and replace it with history from the workspace. + +==== Git Publisher Tags Options + +The git publisher can push tags from the workspace to the remote repository. +Options in this section will allow the plugin to create a new tag. +Options will also allow the plugin to update an existing tag, though the link:https://git-scm.com/docs/git-tag#_on_re_tagging[git documentation] **strongly advises** against updating tags. + +Tag to push:: + + Name of the tag to be pushed from the local workspace to the remote repository. + The name may include link:https://jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables[Jenkins environment variables] or may be a fixed string. + For example, the tag to push might be `$BUILD_TAG`, `my-tag-$BUILD_NUMBER`, `build-$BUILD_NUMBER-from-$NODE_NAME`, or `a-very-specific-string-that-will-be-used-once`. + +Tag message:: + + If the option is selected to create a tag or update a tag, then this message will be associated with the tag that is created. + The message will expand references to link:https://jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables[Jenkins environment variables]. + For example, the message `Build $BUILD_NUMBER tagged on $NODE_NAME` will use the message `Build 1 tagged on master` if build 1 of the job runs on the master. + +Create new tag:: + + Create a new tag in the workspace. + The git publisher will fail the job if the tag already exists. + +Update new tag:: + + Modify existing tag in the workspace so that it points to the most recent commit. + Many git repository hosting services will reject attempts to push a tag which has been modified to point to a different commit than its original commit. + Refer to <> for an option which may force the remote repository to accept a modified tag. + The link:https://git-scm.com/docs/git-tag#_on_re_tagging[git documentation] **strongly advises against updating tags**. + +Tag remote name:: + + Git uses the 'remote name' as a short string replacement for the full URL of the remote repository. + This option defines which remote should receive the push. + This is typically `origin`, though it could be any one of the remote names defined when the plugin performs the checkout. + +==== Git Publisher Branches Options + +The git publisher can push branches from the workspace to the remote repository. +Options in this section will allow the plugin to push the contents of a local branch to the remote repository. + +Branch to push:: + + The name of the remote branch that will receive the latest commits from the agent workspace. + This is usually the same branch that was used for the checkout + +Target remote name:: + + The shortname of the remote that will receive the latest commits from the agent workspace. + Usually this is `origin`. + It needs to be a shortname that is defined in the agent workspace, either through the initial checkout or through later configuration. + +Rebase before push:: + + Some Jenkins jobs may be blocked from pushing changes to the remote repository because the remote repository has received new commits since the start of the job. + This may happen with projects that receive many commits or with projects that have long running jobs. + The `Rebase before push` option fetches the most recent commits from the remote repository, applies local changes over the most recent commits, then pushes the result. + The plugin uses `git rebase` to apply the local changes over the most recent remote changes. ++ +Because `Rebase before push` is modifying the commits in the agent workspace **after the job has completed**, it is creating a configuration of commits that has **not been evaluated by any Jenkins job**. +The commits in the local workspace have been evaluated by the job. +The most recent commits from the remote repository have not been evaluated by the job. +Users may find that the risk of pushing an untested configuration is less than the risk of delaying the visibility of the changes which have been evaluated by the job. + +[[combining-repositories]] +== Combining repositories + +A single reference repository may contain commits from multiple repositories. +For example, if a repository named `parent` includes references to submodules `child-1` and `child-2`, a reference repository could be created to cache commits from all three repositories using the commands: + +.... +$ mkdir multirepository-cache.git +$ cd multirepository-cache.git +$ git init --bare +$ git remote add parent https://github.com/jenkinsci/git-plugin +$ git remote add child-1 https://github.com/jenkinsci/git-client-plugin +$ git remote add child-2 https://github.com/jenkinsci/platformlabeler-plugin +$ git fetch --all +.... + +Those commands create a single bare repository with the current commits from all three repositories. +If that reference repository is used in the advanced clone options link:#clone-reference-repository-path[clone reference repository], it will reduce data transfer and disc use for the parent repository. +If that reference repository is used in the submodule options link:#submodule-reference-repository-path[clone reference repository], it will reduce data transfer and disc use for the submodule repositories. + +[[bug-reports]] +== Bug Reports + +Report issues and enhancements in the +https://issues.jenkins-ci.org[Jenkins issue tracker]. + +[[contributing-to-the-plugin]] +== Contributing to the Plugin + +Refer to link:CONTRIBUTING.adoc#contributing-to-the-git-plugin[contributing to the plugin] for contribution guidelines. +Refer to link:Priorities.adoc#git-plugin-development-priorities[plugin development priorities] for the prioritized list of development topics. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index a19f39d89f..0000000000 --- a/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -buildscript { - repositories { - mavenCentral() - mavenLocal() - } - dependencies { - classpath group: 'org.jenkins-ci.tools', name: 'gradle-jpi-plugin', version: '0.1-SNAPSHOT' - } -} - -apply plugin: 'jpi' - -jenkinsPlugin { - coreVersion = '1.420' - displayName = 'Git Plugin' - url = 'http://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin' -} - -repositories { - maven { - name 'jgit-repository' - url 'http://download.eclipse.org/jgit/maven' - } - maven { - name "jenkins" - delegate.url("http://repo.jenkins-ci.org/public/") - } -} - -group = "org.jenkinsci.plugins" -version = "1.1.15-SNAPSHOT" -archivesBaseName = "git" -description = 'Integrates Jenkins with GIT SCM' -dependencies { - groovy group: 'org.codehaus.groovy', name: 'groovy', version: '1.8.2' - - optionalJenkinsPlugins([group: 'org.jenkins-ci.plugins', name: 'token-macro', version: '1.0', ext: 'jar']) - optionalJenkinsPlugins([group: 'org.jvnet.hudson.plugins', name: 'parameterized-trigger', version: '2.4', ext: 'jar']) { - exclude group: 'org.jvnet.hudson.plugins', module: 'subversion' - } - - compile( - [group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '0.12.1'], - [group: 'joda-time', name: 'joda-time', version: '1.5.1'], - [group: 'com.infradna.tool', name: 'bridge-method-annotation', version: '1.4']) - - testCompile( - [group: 'org.mockito', name: 'mockito-all', version: '1.8.5'], - [group: 'junit', name: 'junit', version: '3.8.1'] - ) -} - -compileJava.targetCompatibility = "1.6" -compileJava.sourceCompatibility = "1.6" - diff --git a/essentials.yml b/essentials.yml new file mode 100644 index 0000000000..3b0d0e27ec --- /dev/null +++ b/essentials.yml @@ -0,0 +1,13 @@ +--- +ath: + athRevision: "dad333092159cb368efc2f9869572f0a05d255ac" + jenkins: "2.122" + tests: + - "GitPluginTest" + - "GitUserContentTest" + - "MultipleScmsPluginTest" + - "WorkflowPluginTest#hello_world_from_git" + - "WorkflowPluginTest#testSharedLibraryFromGithub" +pct: + plugins: + - "git" diff --git a/images/signe-1923369_640.png b/images/signe-1923369_640.png new file mode 100644 index 0000000000..0dad4251a2 Binary files /dev/null and b/images/signe-1923369_640.png differ diff --git a/pom.xml b/pom.xml index 57038f4aa5..e573f3cb32 100644 --- a/pom.xml +++ b/pom.xml @@ -1,343 +1,284 @@ - + + 4.0.0 org.jenkins-ci.plugins plugin - 1.509 + 3.56 + - + The MIT License (MIT) - http://opensource.org/licenses/MIT + https://opensource.org/licenses/MIT repo git - 2.2.2-SNAPSHOT + 4.1.0 hpi - Jenkins GIT plugin + Jenkins Git plugin Integrates Jenkins with GIT SCM - http://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin + https://github.com/jenkinsci/git-plugin/README.adoc + 2007 + + + 4.1.0 + -SNAPSHOT + 2.138.4 + 8 + false + true + 3 + false + 1.35 + - - - - org.apache.maven.plugins - maven-enforcer-plugin - 1.0-beta-1 - - - org.jvnet.localizer - maven-localizer-plugin - 1.14 - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - [1.0,) - - display-info - - - - - - - - - org.codehaus.gmaven - gmaven-plugin - [1.3,) - - generateTestStubs - testCompile - - - - - - - - - com.infradna.tool - bridge-method-injector - [1.4,) - - process - - - - - - - - - - - - org.apache.maven.plugins - maven-site-plugin - 3.3 - - - - - org.jenkins-ci.tools - maven-hpi-plugin - 1.96 - true - - - com.infradna.tool - bridge-method-injector - 1.4 - - - - process - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.5 - 1.5 - - - - org.apache.maven.plugins - maven-release-plugin - 2.3.2 - - deploy - - org.apache.maven.plugins - maven-surefire-plugin - 2.16 + maven-checkstyle-plugin + 3.1.0 - - true - - - ${surefireArgLine} + google_checks.xml + true - - org.jacoco - jacoco-maven-plugin - 0.6.4.201312101107 - - - pre-unit-test - - prepare-agent - - - - ${project.build.directory}/coverage-reports/jacoco-ut.exec - - - surefireArgLine - - - - post-unit-test - test - - report - - - - ${project.build.directory}/coverage-reports/jacoco-ut.exec - - ${project.reporting.outputDirectory}/jacoco-ut - - - - - - - UTF-8 - UTF-8 - - - jgit-repository - Eclipse JGit Repository - https://repo.eclipse.org/content/groups/releases/ - - - - guice-maven - guice maven - http://guice-maven.googlecode.com/svn/trunk - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ - - - kohsuke - Kohsuke Kawaguchi - - - ndeloof - Nicolas De Loof - nicolas.deloof@gmail.com - - - + + + markewaite + Mark Waite + mark.earl.waite@gmail.com + + + rsandell + Robert Sandell + rsandell@cloudbees.com + http://rsandell.com + + + fcojfernandez + Francisco J. Fernández + fjfernandez@cloudbees.com + + - org.eclipse.jgit - org.eclipse.jgit - 3.3.1.201403241930-r + org.jenkins-ci.plugins + structs - joda-time - joda-time - 2.3 + org.jenkins-ci.plugins + git-client + 3.0.0 - com.google.guava - guava - 11.0.1 + org.jenkins-ci.plugins + credentials - com.infradna.tool - bridge-method-annotation - 1.12 + org.jenkins-ci.plugins + ssh-credentials - org.jenkins-ci.plugins - git-client - 1.8.0 + scm-api org.jenkins-ci.plugins - credentials - 1.10 + script-security + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + + org.jenkins-ci.plugins.workflow + workflow-scm-step org.jenkins-ci.plugins - ssh-credentials - 1.6.1 + matrix-project + true org.jenkins-ci.plugins - scm-api - 0.2 + mailer - - junit + org.jenkins-ci.plugins junit - 4.11 + test + + + org.hamcrest + hamcrest-core + 2.2 test org.mockito - mockito-all - 1.9.5 + mockito-core test - org.apache.httpcomponents - httpclient - 4.3.3 + nl.jqno.equalsverifier + equalsverifier + 3.1.11 test - - org.jvnet.hudson.plugins + org.jenkins-ci.plugins parameterized-trigger - 2.4 + 2.33 true - - - org.jvnet.hudson.plugins - subversion - - org.jenkins-ci.plugins token-macro - 1.10 true org.jenkins-ci.plugins multiple-scms - 0.2 - true + 0.6 + test + org.jenkins-ci.plugins promoted-builds - 2.17 + 3.2 + true + + + org.jenkins-ci.plugins.workflow + workflow-step-api + tests + test + + + org.jenkins-ci.plugins + scm-api + tests + test + + + org.jenkins-ci.plugins.workflow + workflow-job + test + + + org.jenkins-ci.plugins.workflow + workflow-basic-steps + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + test + + + org.jenkins-ci.plugins.workflow + workflow-multibranch + 2.21 + test + + + org.jenkins-ci.plugins.workflow + workflow-cps-global-lib + test + + + org.apache.commons + commons-lang3 + + + + + org.xmlunit + xmlunit-matchers + 2.6.3 + test + + + org.jenkins-ci.plugins + git-tag-message + 1.6.1 + test + + + org.jenkins-ci.plugins + git + + + + + + io.jenkins + configuration-as-code + ${configuration-as-code.version} true + + + io.jenkins.configuration-as-code + test-harness + ${configuration-as-code.version} + test + + + + org.jenkins-ci.main + jenkins-test-harness + test - org.sonatype.sisu - sisu-guava + commons-net + commons-net - - - maven.jenkins-ci.org - http://maven.jenkins-ci.org:8081/content/repositories/releases/ - - - + + + + io.jenkins.tools.bom + bom-2.138.x + 4 + import + pom + + + + scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git - http://github.com/jenkinsci/${project.artifactId}-plugin - HEAD + https://github.com/jenkinsci/${project.artifactId}-plugin + git-4.1.0 - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + diff --git a/src/main/java/hudson/plugins/git/BranchSpec.java b/src/main/java/hudson/plugins/git/BranchSpec.java index bb0bb5ab53..51bdae2b92 100644 --- a/src/main/java/hudson/plugins/git/BranchSpec.java +++ b/src/main/java/hudson/plugins/git/BranchSpec.java @@ -4,14 +4,22 @@ import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; +import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.StringTokenizer; +import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; + +import edu.umd.cs.findbugs.annotations.NonNull; /** * A specification of branches to build. Rather like a refspec. @@ -24,11 +32,14 @@ * origin/*/thing * */ +@ExportedBean public class BranchSpec extends AbstractDescribableImpl implements Serializable { private static final long serialVersionUID = -6177158367915899356L; private String name; - + + @Exported + @Whitelisted public String getName() { return name; } @@ -56,17 +67,53 @@ public boolean matches(String item) { return matches(item, env); } - public boolean matches(String item, EnvVars env) { - return getPattern(env).matcher(item).matches(); + /** + * Compare a git branch reference to configured pattern. + *

+ * reference uses normalized format `ref/(heads|tags)/xx` + * pattern do support + *

    + *
  • ref/heads/branch
  • + *
  • (remote-name)?/branch
  • + *
  • ref/remotes/branch
  • + *
  • tag
  • + *
  • (commit sha1)
  • + *
+ * @param ref branch reference to compare + * @param env environment variables to use in comparison + * @return true if ref matches configured pattern + */ + public boolean matches(String ref, EnvVars env) { + return getPattern(env).matcher(ref).matches(); } - + + /** + * Compare the configured pattern to a git branch defined by the repository name and branch name. + * @param repositoryName git repository name + * @param branchName git branch name + * @return true if repositoryName/branchName matches this BranchSpec + */ + public boolean matchesRepositoryBranch(String repositoryName, String branchName) { + if (branchName == null) { + return false; + } + Pattern pattern = getPattern(new EnvVars(), repositoryName); + String branchWithoutRefs = cutRefs(branchName); + return pattern.matcher(branchWithoutRefs).matches() || pattern.matcher(join(repositoryName, branchWithoutRefs)).matches(); + } + + /** + * @deprecated use {@link #filterMatching(Collection, EnvVars)} + * @param branches source branch list to be filtered by configured branch specification using a newly constructed EnvVars + * @return branch names which match + */ public List filterMatching(Collection branches) { EnvVars env = new EnvVars(); return filterMatching(branches, env); } public List filterMatching(Collection branches, EnvVars env) { - List items = new ArrayList(); + List items = new ArrayList<>(); for(String b : branches) { if(matches(b, env)) @@ -82,7 +129,7 @@ public List filterMatchingBranches(Collection branches) { } public List filterMatchingBranches(Collection branches, EnvVars env) { - List items = new ArrayList(); + List items = new ArrayList<>(); for(Branch b : branches) { if(matches(b.getName(), env)) @@ -93,33 +140,60 @@ public List filterMatchingBranches(Collection branches, EnvVars } private String getExpandedName(EnvVars env) { - return env.expand(name); + String expandedName = env.expand(name); + if (expandedName.length() == 0) { + return "**"; + } + return expandedName; } - + private Pattern getPattern(EnvVars env) { + return getPattern(env, null); + } + + private Pattern getPattern(EnvVars env, String repositoryName) { String expandedName = getExpandedName(env); // use regex syntax directly if name starts with colon if (expandedName.startsWith(":") && expandedName.length() > 1) { String regexSubstring = expandedName.substring(1, expandedName.length()); return Pattern.compile(regexSubstring); } - - // if an unqualified branch was given add a "*/" so it will match branches - // from remote repositories as the user probably intended - String qualifiedName; - if (!expandedName.contains("**") && !expandedName.contains("/")) - qualifiedName = "*/" + expandedName; - else - qualifiedName = expandedName; - + if (repositoryName != null) { + // remove the "refs/.../" stuff from the branch-spec if necessary + String pattern = cutRefs(expandedName) + // remove a leading "remotes/" from the branch spec + .replaceAll("^remotes/", ""); + pattern = convertWildcardStringToRegex(pattern); + return Pattern.compile(pattern); + } + // build a pattern into this builder StringBuilder builder = new StringBuilder(); - + + // for legacy reasons (sic) we do support various branch spec format to declare remotes / branches + builder.append("(refs/heads/"); + + + // if an unqualified branch was given, consider all remotes (with various possible syntaxes) + // so it will match branches from any remote repositories as the user probably intended + if (!expandedName.contains("**") && !expandedName.contains("/")) { + builder.append("|refs/remotes/[^/]+/|remotes/[^/]+/|[^/]+/"); + } else { + builder.append("|refs/remotes/|remotes/"); + } + builder.append(")?"); + builder.append(convertWildcardStringToRegex(expandedName)); + return Pattern.compile(builder.toString()); + } + + private String convertWildcardStringToRegex(String expandedName) { + StringBuilder builder = new StringBuilder(); + // was the last token a wildcard? boolean foundWildcard = false; // split the string at the wildcards - StringTokenizer tokenizer = new StringTokenizer(qualifiedName, "*", true); + StringTokenizer tokenizer = new StringTokenizer(expandedName, "*", true); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); @@ -154,8 +228,16 @@ private Pattern getPattern(EnvVars env) { if (foundWildcard) { builder.append("[^/]*"); } - - return Pattern.compile(builder.toString()); + return builder.toString(); + } + + private String cutRefs(@NonNull String name) { + Matcher matcher = GitSCM.GIT_REF.matcher(name); + return matcher.matches() ? matcher.group(2) : name; + } + + private String join(String repositoryName, String branchWithoutRefs) { + return StringUtils.join(Arrays.asList(repositoryName, branchWithoutRefs), "/"); } @Extension diff --git a/src/main/java/hudson/plugins/git/ChangelogToBranchOptions.java b/src/main/java/hudson/plugins/git/ChangelogToBranchOptions.java new file mode 100644 index 0000000000..691d296213 --- /dev/null +++ b/src/main/java/hudson/plugins/git/ChangelogToBranchOptions.java @@ -0,0 +1,49 @@ +package hudson.plugins.git; + +import java.io.Serializable; +import org.kohsuke.stapler.DataBoundConstructor; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; + +/** + * Options for the {@link hudson.plugins.git.extensions.impl.ChangelogToBranch} extension. + * + * @author Dirk Reske (dirk.reske@t-systems.com) + */ +public class ChangelogToBranchOptions extends AbstractDescribableImpl implements Serializable { + private String compareRemote; + private String compareTarget; + + @DataBoundConstructor + public ChangelogToBranchOptions(String compareRemote, String compareTarget) { + this.compareRemote = compareRemote; + this.compareTarget = compareTarget; + } + + public ChangelogToBranchOptions(ChangelogToBranchOptions options) { + this(options.getCompareRemote(), options.getCompareTarget()); + } + + public String getCompareRemote() { + return compareRemote; + } + + public String getCompareTarget() { + return compareTarget; + } + + public String getRef() { + return compareRemote + "/" + compareTarget; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return ""; + } + } +} diff --git a/src/main/java/hudson/plugins/git/GitBranchSpecifierColumn.java b/src/main/java/hudson/plugins/git/GitBranchSpecifierColumn.java new file mode 100644 index 0000000000..dd8c63119b --- /dev/null +++ b/src/main/java/hudson/plugins/git/GitBranchSpecifierColumn.java @@ -0,0 +1,59 @@ +package hudson.plugins.git; + +import hudson.views.ListViewColumn; +import hudson.Extension; +import hudson.model.Item; +import hudson.scm.SCM; +import hudson.views.ListViewColumnDescriptor; +import java.util.ArrayList; +import java.util.List; +import jenkins.triggers.SCMTriggerItem; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Class that adds an optional 'Git branches to build' column to a list view. + * + * @author Mads + */ +public class GitBranchSpecifierColumn extends ListViewColumn { + + @DataBoundConstructor + public GitBranchSpecifierColumn() { } + + public List getBranchSpecifier( final Item item ) { + List branchSpec = new ArrayList<>(); + SCMTriggerItem s = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(item); + if(s != null) { + for(SCM scm : s.getSCMs()) { + if (scm instanceof GitSCM) { + GitSCM gitScm = (GitSCM)scm; + for(BranchSpec spec : gitScm.getBranches()) { + branchSpec.add(spec.getName()); + } + } + } + } + return branchSpec; + } + + public String breakOutString(List branches) { + return StringUtils.join(branches, ", "); + } + + @Extension + public static class DescriptorImpl extends ListViewColumnDescriptor { + + @Override + public String getDisplayName() { + return "Git Branches"; + } + + @Override + public boolean shownByDefault() { + return false; + } + + } + +} diff --git a/src/main/java/hudson/plugins/git/GitChangeLogParser.java b/src/main/java/hudson/plugins/git/GitChangeLogParser.java index e675e56124..fda5a9c700 100644 --- a/src/main/java/hudson/plugins/git/GitChangeLogParser.java +++ b/src/main/java/hudson/plugins/git/GitChangeLogParser.java @@ -1,21 +1,21 @@ package hudson.plugins.git; -import hudson.model.AbstractBuild; +import hudson.model.Run; import hudson.scm.ChangeLogParser; +import hudson.scm.RepositoryBrowser; +import org.jenkinsci.plugins.gitclient.CliGitAPIImpl; +import org.jenkinsci.plugins.gitclient.GitClient; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; import org.xml.sax.SAXException; import javax.annotation.Nonnull; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; +import java.io.InputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; @@ -29,33 +29,67 @@ public class GitChangeLogParser extends ChangeLogParser { private boolean authorOrCommitter; + private boolean showEntireCommitSummaryInChanges; + /** + * Git client plugin 2.x silently truncated the first line of a commit message when showing the changelog summary in + * the 'Changes' page using command line git. They did not truncate when using JGit. In order to simplify the git + * client plugin implementation, the truncation was removed from git client plugin 3.0. In order to retain backward + * compatibility, git plugin 4.0 became responsible to truncate the summary at the correct points. + * As a result of that change of responsibility, this class needs to know which implementation is being used so + * that it can adapt for appropriate compatibility. + * + * @param authorOrCommitter read author name instead of committer name if true + * @deprecated use #GitChangeLogParser(GitClient, boolean) + */ + @Deprecated public GitChangeLogParser(boolean authorOrCommitter) { + this(null, authorOrCommitter); + } + + /** + * Git client plugin 2.x silently truncated the first line of a commit message when showing the changelog summary in + * the 'Changes' page using command line git. They did not truncate when using JGit. In order to simplify the git + * client plugin implementation, the truncation was removed from git client plugin 3.0. In order to retain backward + * compatibility, git plugin 4.0 became responsible to truncate the summary at the correct points. + * As a result of that change of responsibility, this class needs to know which implementation is being used so + * that it can adapt for compatibility. + * + * @param git the GitClient implementation to be used by the change log parser + * @param authorOrCommitter read author name instead of committer name if true + */ + public GitChangeLogParser(GitClient git, boolean authorOrCommitter) { super(); this.authorOrCommitter = authorOrCommitter; + /* Retain full commit summary if globally configured to retain full commit summary or if not using command line git. + * That keeps change summary truncation compatible with git client plugin 2.x and git plugin 3.x for users of + * command line git. + */ + this.showEntireCommitSummaryInChanges = GitChangeSet.isShowEntireCommitSummaryInChanges() || !(git instanceof CliGitAPIImpl); + } + + public List parse(@Nonnull InputStream changelog) throws IOException { + return parse(IOUtils.readLines(changelog, "UTF-8")); } public List parse(@Nonnull List changelog) { return parse(changelog.iterator()); } - public GitChangeSetList parse(AbstractBuild build, File changelogFile) + @Override public GitChangeSetList parse(Run build, RepositoryBrowser browser, File changelogFile) throws IOException, SAXException { - - Set r = new LinkedHashSet(); - // Parse the log file into GitChangeSet items - each one is a commit LineIterator lineIterator = null; try { - lineIterator = FileUtils.lineIterator(changelogFile); - return new GitChangeSetList(build, parse(lineIterator)); + lineIterator = FileUtils.lineIterator(changelogFile,"UTF-8"); + return new GitChangeSetList(build, browser, parse(lineIterator)); } finally { LineIterator.closeQuietly(lineIterator); } } private List parse(Iterator changelog) { - Set r = new LinkedHashSet(); + Set r = new LinkedHashSet<>(); List lines = null; while (changelog.hasNext()) { String line = changelog.next(); @@ -63,7 +97,7 @@ private List parse(Iterator changelog) { if (lines != null) { r.add(parseCommit(lines, authorOrCommitter)); } - lines = new ArrayList(); + lines = new ArrayList<>(); } if (lines != null && lines.size() parse(Iterator changelog) { if (lines != null) { r.add(parseCommit(lines, authorOrCommitter)); } - return new ArrayList(r); + return new ArrayList<>(r); } private GitChangeSet parseCommit(List lines, boolean authorOrCommitter) { - return new GitChangeSet(lines, authorOrCommitter); + return new GitChangeSet(lines, authorOrCommitter, showEntireCommitSummaryInChanges); } /** diff --git a/src/main/java/hudson/plugins/git/GitChangeSet.java b/src/main/java/hudson/plugins/git/GitChangeSet.java index 962797ac41..83d71694d3 100644 --- a/src/main/java/hudson/plugins/git/GitChangeSet.java +++ b/src/main/java/hudson/plugins/git/GitChangeSet.java @@ -1,30 +1,41 @@ package hudson.plugins.git; import hudson.MarkupText; -import hudson.model.Hudson; +import hudson.Plugin; import hudson.model.User; import hudson.plugins.git.GitSCM.DescriptorImpl; import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.AffectedFile; import hudson.scm.EditType; -import org.apache.commons.lang.time.FastDateFormat; +import jenkins.model.Jenkins; +import org.apache.commons.lang.math.NumberUtils; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; +import javax.annotation.CheckForNull; import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import static hudson.Util.fixEmpty; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; + /** * Represents a change set. * @author Nigel Magnay @@ -43,7 +54,13 @@ public class GitChangeSet extends ChangeLogSet.Entry { private static final Pattern RENAME_SPLIT = Pattern.compile("^(.*?)\t(.*)$"); private static final String NULL_HASH = "0000000000000000000000000000000000000000"; - private static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss"; + private static final String ISO_8601_WITH_TZ = "yyyy-MM-dd'T'HH:mm:ssX"; + static final int TRUNCATE_LIMIT = 72; + + private final DateTimeFormatter [] dateFormatters; + + public static final Logger LOGGER = Logger.getLogger(GitChangeSet.class.getName()); /** * This is broken as a part of the 1.5 refactoring. @@ -64,7 +81,6 @@ public class GitChangeSet extends ChangeLogSet.Entry { * the commit graph and see if a commit can be only reachable from the "revOfBranchInPreviousBuild" of * just one branch, in which case it's safe to attribute the commit to that branch. */ - private String branch; private String committer; private String committerEmail; private String committerTime; @@ -75,20 +91,86 @@ public class GitChangeSet extends ChangeLogSet.Entry { private String title; private String id; private String parentCommit; - private Collection paths = new HashSet(); + private Collection paths = new HashSet<>(); private boolean authorOrCommitter; + private boolean showEntireCommitSummaryInChanges; /** - * Create Git change set using information in given lines + * Create Git change set using information in given lines. * - * @param lines - * @param authorOrCommitter + * @param lines change set lines read to construct change set + * @param authorOrCommitter if true, use author information (name, time), otherwise use committer information */ public GitChangeSet(List lines, boolean authorOrCommitter) { + this(lines, authorOrCommitter, isShowEntireCommitSummaryInChanges()); + } + + /* Add time zone parsing for +00:00 offset, +0000 offset, and +00 offset */ + private DateTimeFormatterBuilder addZoneOffset(DateTimeFormatterBuilder builder) { + builder.optionalStart().appendOffset("+HH:MM", "+00:00").optionalEnd(); + builder.optionalStart().appendOffset("+HHMM", "+0000").optionalEnd(); + builder.optionalStart().appendOffset("+HH", "Z").optionalEnd(); + return builder; + } + + /** + * Create Git change set using information in given lines. + * + * @param lines change set lines read to construct change set + * @param authorOrCommitter if true, use author information (name, time), otherwise use committer information + * @param retainFullCommitSummary if true, do not truncate commit summary in the 'Changes' page + */ + public GitChangeSet(List lines, boolean authorOrCommitter, boolean retainFullCommitSummary) { this.authorOrCommitter = authorOrCommitter; + this.showEntireCommitSummaryInChanges = retainFullCommitSummary; if (lines.size() > 0) { parseCommit(lines); } + + // Nearly ISO dates generated by git whatchanged --format=+ci + // Look like '2015-09-30 08:21:24 -0600' + // ISO is '2015-09-30T08:21:24-06:00' + // Uses Builder rather than format pattern for more reliable parsing + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); + builder.append(DateTimeFormatter.ISO_LOCAL_DATE); + builder.appendLiteral(' '); + builder.append(DateTimeFormatter.ISO_LOCAL_TIME); + builder.optionalStart().appendLiteral(' ').optionalEnd(); + addZoneOffset(builder); + DateTimeFormatter gitDateFormatter = builder.toFormatter(); + + // DateTimeFormat.forPattern("yyyy-MM-DDTHH:mm:ssZ"); + // 2013-03-21T15:16:44+0100 + // Uses Builder rather than format pattern for more reliable parsing + builder = new DateTimeFormatterBuilder(); + builder.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + addZoneOffset(builder); + DateTimeFormatter nearlyISOFormatter = builder.toFormatter(); + + dateFormatters = new DateTimeFormatter[3]; + dateFormatters[0] = gitDateFormatter; // First priority +%cI format + dateFormatters[1] = nearlyISOFormatter; // Second priority seen in git-plugin + dateFormatters[2] = DateTimeFormatter.ISO_OFFSET_DATE_TIME; // Third priority, ISO 8601 format + } + + /** + * The git client plugin command line implementation silently truncated changelog summaries (the first line of the + * commit message) that were longer than 72 characters beginning with git client plugin 2.0. Beginning with git + * client plugin 3.0 and git plugin 4.0, the git client plugin no longer silently truncates changelog summaries. + * Truncation responsibility has moved into the git plugin. The git plugin will default to truncate all changelog + * summaries (including JGit summaries) unless title truncation has been globally disabled or the caller called the + * GitChangeSet constructor with the argument to retain the full commit summary. + * + * See JENKINS-29977 for more details + * + * @return true if first line of commit message should be truncated at word boundary before 73 characters + */ + static boolean isShowEntireCommitSummaryInChanges() { + try { + return new DescriptorImpl().isShowEntireCommitSummaryInChanges(); + }catch (Throwable t){ + return false; + } } private void parseCommit(List lines) { @@ -99,10 +181,14 @@ private void parseCommit(List lines) { if( line.length() < 1) continue; if (line.startsWith("commit ")) { - this.id = line.split(" ")[1]; + String[] split = line.split(" "); + if (split.length > 1) this.id = split[1]; + else throw new IllegalArgumentException("Commit has no ID" + lines); } else if (line.startsWith("tree ")) { } else if (line.startsWith("parent ")) { - this.parentCommit = line.split(" ")[1]; + String[] split = line.split(" "); + // parent may be null for initial commit or changelog computed from a shallow clone + if (split.length > 1) this.parentCommit = split[1]; } else if (line.startsWith(PREFIX_COMMITTER)) { Matcher committerMatcher = COMMITTER_ENTRY.matcher(line); if (committerMatcher.matches() @@ -160,25 +246,55 @@ else if (editMode == 'C') { } } } - this.comment = message.toString(); - int endOfFirstLine = this.comment.indexOf('\n'); if (endOfFirstLine == -1) { - this.title = this.comment; + this.title = this.comment.trim(); } else { - this.title = this.comment.substring(0, endOfFirstLine); + this.title = this.comment.substring(0, endOfFirstLine).trim(); + } + if(!showEntireCommitSummaryInChanges){ + this.title = splitString(this.title, TRUNCATE_LIMIT); + } + } + + /* Package protected for testing */ + static String splitString(String msg, int lineSize) { + if (msg == null) return ""; + if (msg.matches(".*[\r\n].*")) { + String [] msgArray = msg.split("[\r\n]"); + msg = msgArray[0]; } + if (msg.length() <= lineSize || !msg.contains(" ")) { + return msg; + } + int lastSpace = msg.lastIndexOf(' ', lineSize); + if (lastSpace == -1) { + /* String contains a space but space is outside truncation limit, truncate at first space */ + lastSpace = msg.indexOf(' '); + } + return (lastSpace == -1) ? msg : msg.substring(0, lastSpace); } /** Convert to iso date format if required */ private String isoDateFormat(String s) { - if (s.length() == 25 /* already in ISO 8601 */) return s; - - // legacy mode - int i = s.indexOf(' '); - long time = Long.parseLong(s.substring(0,i)); - return FastDateFormat.getInstance(ISO_8601).format(new Date(time)) + s.substring(i); + String date = s; + String timezone = "Z"; + int spaceIndex = s.indexOf(' '); + if (spaceIndex > 0) { + date = s.substring(0, spaceIndex); + timezone = s.substring(spaceIndex+1); + } + if (NumberUtils.isDigits(date)) { + // legacy mode + long time = Long.parseLong(date); + DateFormat formatter = new SimpleDateFormat(ISO_8601); + formatter.setTimeZone(TimeZone.getTimeZone("GMT")); + return formatter.format(new Date(time * 1000)) + timezone; + } else { + // already in ISO format + return s; + } } private String parseHash(String hash) { @@ -190,11 +306,34 @@ public String getDate() { return authorOrCommitter ? authorTime : committerTime; } + @Exported + public String getAuthorEmail() { + return authorOrCommitter ? authorEmail : committerEmail; + } + @Override public long getTimestamp() { + String date = getDate(); + if (date == null) { + LOGGER.log(Level.WARNING, "Failed to parse null date"); + return -1; + } + if (date.isEmpty()) { + LOGGER.log(Level.WARNING, "Failed to parse empty date"); + return -1; + } + + for (DateTimeFormatter dateFormatter : dateFormatters) { + try { + ZonedDateTime dateTime = ZonedDateTime.parse(date, dateFormatter); + return dateTime.toEpochSecond()* 1000L; + } catch (DateTimeParseException | IllegalArgumentException e) { + } + } try { - return new SimpleDateFormat(ISO_8601).parse(getDate()).getTime(); - } catch (ParseException e) { + LOGGER.log(Level.FINE, "Parsing {0} with SimpleDateFormat because other parsers failed", date); + return new SimpleDateFormat(ISO_8601_WITH_TZ).parse(date).getTime(); + } catch (IllegalArgumentException | ParseException e) { return -1; } } @@ -209,14 +348,14 @@ public void setParent(ChangeLogSet parent) { super.setParent(parent); } - public String getParentCommit() { + public @CheckForNull + String getParentCommit() { return parentCommit; } - @Override public Collection getAffectedPaths() { - Collection affectedPaths = new HashSet(this.paths.size()); + Collection affectedPaths = new HashSet<>(this.paths.size()); for (Path file : this.paths) { affectedPaths.add(file.getPath()); } @@ -245,31 +384,84 @@ public Collection getAffectedFiles() { * @param csAuthorEmail user email. * @param createAccountBasedOnEmail true if create new user based on committer's email. * @return {@link User} + * @deprecated Use {@link #findOrCreateUser(String,String,boolean,boolean)} */ + @Deprecated public User findOrCreateUser(String csAuthor, String csAuthorEmail, boolean createAccountBasedOnEmail) { + return findOrCreateUser(csAuthor, csAuthorEmail, createAccountBasedOnEmail, false); + } + + /** + * Returns user of the change set. + * + * @param csAuthor user name. + * @param csAuthorEmail user email. + * @param createAccountBasedOnEmail true if create new user based on committer's email. + * @param useExistingAccountWithSameEmail true if users should be searched for their email attribute + * @return {@link User} + */ + public User findOrCreateUser(String csAuthor, String csAuthorEmail, boolean createAccountBasedOnEmail, + boolean useExistingAccountWithSameEmail) { User user; + if (csAuthor == null) { + return User.getUnknown(); + } if (createAccountBasedOnEmail) { - user = User.get(csAuthorEmail, false); + if (csAuthorEmail == null || csAuthorEmail.isEmpty()) { + // Avoid exception from User.get("", false) + return User.getUnknown(); + } + user = User.get(csAuthorEmail, false, Collections.emptyMap()); if (user == null) { try { - user = User.get(csAuthorEmail, true); - user.setFullName(csAuthor); - if (hasHudsonTasksMailer()) - setMail(user, csAuthorEmail); - user.save(); + user = User.get(csAuthorEmail, !useExistingAccountWithSameEmail, Collections.emptyMap()); + boolean setUserDetails = true; + if (user == null && useExistingAccountWithSameEmail && hasMailerPlugin()) { + for(User existingUser : User.getAll()) { + if (csAuthorEmail.equalsIgnoreCase(getMail(existingUser))) { + user = existingUser; + setUserDetails = false; + break; + } + } + } + if (user == null) { + user = User.get(csAuthorEmail, true, Collections.emptyMap()); + } + if (setUserDetails) { + user.setFullName(csAuthor); + if (hasMailerPlugin()) + setMail(user, csAuthorEmail); + user.save(); + } } catch (IOException e) { // add logging statement? } } } else { - user = User.get(csAuthor, false); + if (csAuthor.isEmpty()) { + // Avoid exception from User.get("", false) + return User.getUnknown(); + } + user = User.get(csAuthor, false, Collections.emptyMap()); - if (user == null) - user = User.get(csAuthorEmail.split("@")[0], true); + if (user == null) { + if (csAuthorEmail == null || csAuthorEmail.isEmpty()) { + return User.getUnknown(); + } + // Ensure that malformed email addresses (in this case, just '@') + // don't mess us up. + String[] emailParts = csAuthorEmail.split("@"); + if (emailParts.length > 0) { + user = User.get(emailParts[0], true, Collections.emptyMap()); + } else { + return User.getUnknown(); + } + } } // set email address for user if none is already available - if (fixEmpty(csAuthorEmail) != null && hasHudsonTasksMailer() && !hasMail(user)) { + if (fixEmpty(csAuthorEmail) != null && hasMailerPlugin() && !hasMail(user)) { try { setMail(user, csAuthorEmail); } catch (IOException e) { @@ -279,31 +471,54 @@ public User findOrCreateUser(String csAuthor, String csAuthorEmail, boolean crea return user; } + private String getMail(User user) { + hudson.tasks.Mailer.UserProperty property = user.getProperty(hudson.tasks.Mailer.UserProperty.class); + if (property == null) { + return null; + } + if (!property.hasExplicitlyConfiguredAddress()) { + return null; + } + return property.getExplicitlyConfiguredAddress(); + } + private void setMail(User user, String csAuthorEmail) throws IOException { user.addProperty(new hudson.tasks.Mailer.UserProperty(csAuthorEmail)); } private boolean hasMail(User user) { - hudson.tasks.Mailer.UserProperty property = user.getProperty(hudson.tasks.Mailer.UserProperty.class); - return property != null && property.hasExplicitlyConfiguredAddress(); - } + String email = getMail(user); + return email != null; + } - private boolean hasHudsonTasksMailer() { - // TODO convert to checking for mailer plugin as plugin migrates to 1.509+ - try { - Class.forName("hudson.tasks.Mailer"); - return true; - } catch (ClassNotFoundException e) { - return false; + private boolean hasMailerPlugin() { + Plugin p = Jenkins.get().getPlugin("mailer"); + if (p != null) { + return p.getWrapper().isActive(); } + return false; } - private boolean isCreateAccountBasedOnEmail() { - DescriptorImpl descriptor = (DescriptorImpl) Hudson.getInstance().getDescriptor(GitSCM.class); + private boolean isCreateAccountBasedOnEmail() { + DescriptorImpl descriptor = getGitSCMDescriptor(); return descriptor.isCreateAccountBasedOnEmail(); } + private boolean isUseExistingAccountWithSameEmail() { + DescriptorImpl descriptor = getGitSCMDescriptor(); + + if (descriptor == null) { + return false; + } + + return descriptor.isUseExistingAccountWithSameEmail(); + } + + private DescriptorImpl getGitSCMDescriptor() { + return (DescriptorImpl) Jenkins.get().getDescriptor(GitSCM.class); + } + @Override @Exported public User getAuthor() { @@ -320,11 +535,7 @@ public User getAuthor() { csAuthorEmail = this.committerEmail; } - if (csAuthor == null) { - throw new RuntimeException("No author in changeset " + id); - } - - return findOrCreateUser(csAuthor, csAuthorEmail, isCreateAccountBasedOnEmail()); + return findOrCreateUser(csAuthor, csAuthorEmail, isCreateAccountBasedOnEmail(), isUseExistingAccountWithSameEmail()); } /** @@ -337,8 +548,6 @@ public User getAuthor() { public String getAuthorName() { // If true, use the author field from git log rather than the committer. String csAuthor = authorOrCommitter ? author : committer; - if (csAuthor == null) - throw new RuntimeException("No author in changeset " + id); return csAuthor; } @@ -364,17 +573,18 @@ public String getComment() { /** * Gets {@linkplain #getComment() the comment} fully marked up by {@link ChangeLogAnnotator}. + @return annotated comment */ public String getCommentAnnotated() { MarkupText markup = new MarkupText(getComment()); for (ChangeLogAnnotator a : ChangeLogAnnotator.all()) - a.annotate(getParent().build,this,markup); + a.annotate(getParent().getRun(), this, markup); return markup.toString(false); } public String getBranch() { - return this.branch; + return null; } @ExportedBean(defaultVisibility=999) @@ -424,15 +634,22 @@ public EditType getEditType() { } } - public int hashCode() { - return id != null ? id.hashCode() : super.hashCode(); + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + GitChangeSet that = (GitChangeSet) o; + + return id != null && id.equals(that.id); } - public boolean equals(Object obj) { - if (obj == this) - return true; - if (obj instanceof GitChangeSet) - return id != null && id.equals(((GitChangeSet) obj).id); - return false; + @Override + public int hashCode() { + return id != null ? id.hashCode() : super.hashCode(); } } diff --git a/src/main/java/hudson/plugins/git/GitChangeSetList.java b/src/main/java/hudson/plugins/git/GitChangeSetList.java index 523238e894..6b40a4fc86 100644 --- a/src/main/java/hudson/plugins/git/GitChangeSetList.java +++ b/src/main/java/hudson/plugins/git/GitChangeSetList.java @@ -1,7 +1,8 @@ package hudson.plugins.git; -import hudson.model.AbstractBuild; +import hudson.model.Run; import hudson.scm.ChangeLogSet; +import hudson.scm.RepositoryBrowser; import org.kohsuke.stapler.export.Exported; import java.util.Collections; @@ -16,8 +17,8 @@ public class GitChangeSetList extends ChangeLogSet { private final List changeSets; - /*package*/ GitChangeSetList(AbstractBuild build, List logs) { - super(build); + /*package*/ GitChangeSetList(Run build, RepositoryBrowser browser, List logs) { + super(build, browser); Collections.reverse(logs); // put new things first this.changeSets = Collections.unmodifiableList(logs); for (GitChangeSet log : logs) diff --git a/src/main/java/hudson/plugins/git/GitPublisher.java b/src/main/java/hudson/plugins/git/GitPublisher.java index c6d4e16e86..a13d8dc742 100644 --- a/src/main/java/hudson/plugins/git/GitPublisher.java +++ b/src/main/java/hudson/plugins/git/GitPublisher.java @@ -6,10 +6,6 @@ import hudson.FilePath; import hudson.Launcher; import hudson.Util; -import hudson.matrix.MatrixAggregatable; -import hudson.matrix.MatrixAggregator; -import hudson.matrix.MatrixBuild; -import hudson.matrix.MatrixRun; import hudson.model.AbstractBuild; import hudson.model.AbstractDescribableImpl; import hudson.model.AbstractProject; @@ -26,11 +22,10 @@ import hudson.util.FormValidation; import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import org.jenkinsci.plugins.gitclient.GitClient; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.jenkinsci.plugins.gitclient.PushCommand; +import org.kohsuke.stapler.*; import javax.servlet.ServletException; import java.io.IOException; @@ -38,7 +33,7 @@ import java.util.ArrayList; import java.util.List; -public class GitPublisher extends Recorder implements Serializable, MatrixAggregatable { +public class GitPublisher extends Recorder implements Serializable { private static final long serialVersionUID = 1L; /** @@ -49,6 +44,7 @@ public class GitPublisher extends Recorder implements Serializable, MatrixAggreg private boolean pushMerge; private boolean pushOnlyIfSuccess; + private boolean forcePush; private List tagsToPush; // Pushes HEAD to these locations @@ -61,12 +57,14 @@ public GitPublisher(List tagsToPush, List branchesToPush, List notesToPush, boolean pushOnlyIfSuccess, - boolean pushMerge) { + boolean pushMerge, + boolean forcePush) { this.tagsToPush = tagsToPush; this.branchesToPush = branchesToPush; this.notesToPush = notesToPush; this.pushMerge = pushMerge; this.pushOnlyIfSuccess = pushOnlyIfSuccess; + this.forcePush = forcePush; this.configVersion = 2L; } @@ -78,6 +76,10 @@ public boolean isPushMerge() { return pushMerge; } + public boolean isForcePush() { + return forcePush; + } + public boolean isPushTags() { if (tagsToPush == null) { return false; @@ -101,7 +103,7 @@ public boolean isPushNotes() { public List getTagsToPush() { if (tagsToPush == null) { - tagsToPush = new ArrayList(); + tagsToPush = new ArrayList<>(); } return tagsToPush; @@ -109,7 +111,7 @@ public List getTagsToPush() { public List getBranchesToPush() { if (branchesToPush == null) { - branchesToPush = new ArrayList(); + branchesToPush = new ArrayList<>(); } return branchesToPush; @@ -117,7 +119,7 @@ public List getBranchesToPush() { public List getNotesToPush() { if (notesToPush == null) { - notesToPush = new ArrayList(); + notesToPush = new ArrayList<>(); } return notesToPush; @@ -125,37 +127,19 @@ public List getNotesToPush() { public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.BUILD; + return BuildStepMonitor.NONE; } - /** - * For a matrix project, push should only happen once. - */ - public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { - return new MatrixAggregator(build,launcher,listener) { - @Override - public boolean endBuild() throws InterruptedException, IOException { - return GitPublisher.this.perform(build,launcher,listener); - } - }; - } - private String replaceAdditionalEnvironmentalVariables(String input, AbstractBuild build){ if (build == null){ return input; } - String buildResult = build.getResult().toString(); - String buildDuration = build.getDurationString(); - - if ( buildResult == null){ - buildResult = ""; - } - if ( buildDuration == null){ - buildDuration = ""; - } - else{ - buildDuration = buildDuration.replaceAll("and counting", ""); + String buildResult = ""; + Result result = build.getResult(); + if (result != null) { + buildResult = result.toString(); } + String buildDuration = build.getDurationString().replaceAll("and counting", ""); input = input.replaceAll("\\$BUILDRESULT", buildResult); input = input.replaceAll("\\$BUILDDURATION", buildDuration); @@ -169,7 +153,7 @@ public boolean perform(AbstractBuild build, // during matrix build, the push back would happen at the very end only once for the whole matrix, // not for individual configuration build. - if (build instanceof MatrixRun) { + if (build.getClass().getName().equals("hudson.matrix.MatrixRun")) { return true; } @@ -181,11 +165,6 @@ public boolean perform(AbstractBuild build, final GitSCM gitSCM = (GitSCM) scm; - if(gitSCM.getUseShallowClone()) { - listener.getLogger().println("GitPublisher disabled while using shallow clone."); - return true; - } - final String projectName = build.getProject().getName(); final int buildNumber = build.getNumber(); final Result buildResult = build.getResult(); @@ -198,7 +177,9 @@ public boolean perform(AbstractBuild build, else { EnvVars environment = build.getEnvironment(listener); - final GitClient git = gitSCM.createClient(listener,environment,build); + final GitClient git = gitSCM.createClient(listener, environment, build, build.getWorkspace()); + + URIish remoteURI; // If we're pushing the merge back... if (pushMerge) { @@ -221,17 +202,20 @@ public boolean perform(AbstractBuild build, if (mergeOptions.doMerge() && buildResult.isBetterOrEqualTo(Result.SUCCESS)) { RemoteConfig remote = mergeOptions.getMergeRemote(); + + // expand environment variables in remote repository + remote = gitSCM.getParamExpandedRepo(environment, remote); + listener.getLogger().println("Pushing HEAD to branch " + mergeTarget + " of " + remote.getName() + " repository"); - git.push(remote.getName(), "HEAD:" + mergeTarget); + remoteURI = remote.getURIs().get(0); + PushCommand push = git.push().to(remoteURI).ref("HEAD:" + mergeTarget).force(forcePush); + push.execute(); } else { //listener.getLogger().println("Pushing result " + buildnumber + " to origin repository"); //git.push(null); } - } catch (FormException e) { - e.printStackTrace(listener.error("Failed to push merge to origin repository")); - return false; - } catch (GitException e) { + } catch (FormException | GitException e) { e.printStackTrace(listener.error("Failed to push merge to origin repository")); return false; } @@ -250,11 +234,15 @@ public boolean perform(AbstractBuild build, final String targetRepo = environment.expand(t.getTargetRepoName()); try { - RemoteConfig remote = gitSCM.getRepositoryByName(targetRepo); + // Lookup repository with unexpanded name as GitSCM stores them unexpanded + RemoteConfig remote = gitSCM.getRepositoryByName(t.getTargetRepoName()); if (remote == null) throw new AbortException("No repository found for target repo name " + targetRepo); + // expand environment variables in remote repository + remote = gitSCM.getParamExpandedRepo(environment, remote); + boolean tagExists = git.tagExists(tagName.replace(' ', '_')); if (t.isCreateTag() || t.isUpdateTag()) { if (tagExists && !t.isUpdateTag()) { @@ -273,7 +261,10 @@ else if (!tagExists) { listener.getLogger().println("Pushing tag " + tagName + " to repo " + targetRepo); - git.push(remote.getName(), tagName); + + remoteURI = remote.getURIs().get(0); + PushCommand push = git.push().to(remoteURI).ref(tagName).force(forcePush); + push.execute(); } catch (GitException e) { e.printStackTrace(listener.error("Failed to push tag " + tagName + " to " + targetRepo)); return false; @@ -293,14 +284,30 @@ else if (!tagExists) { final String targetRepo = environment.expand(b.getTargetRepoName()); try { - RemoteConfig remote = gitSCM.getRepositoryByName(targetRepo); + // Lookup repository with unexpanded name as GitSCM stores them unexpanded + RemoteConfig remote = gitSCM.getRepositoryByName(b.getTargetRepoName()); if (remote == null) throw new AbortException("No repository found for target repo name " + targetRepo); + // expand environment variables in remote repository + remote = gitSCM.getParamExpandedRepo(environment, remote); + remoteURI = remote.getURIs().get(0); + + if (b.getRebaseBeforePush()) { + listener.getLogger().println("Fetch and rebase with " + branchName + " of " + targetRepo); + git.fetch_().from(remoteURI, remote.getFetchRefSpecs()).execute(); + if (!git.revParse("HEAD").equals(git.revParse(targetRepo + "/" + branchName))) { + git.rebase().setUpstream(targetRepo + "/" + branchName).execute(); + } else { + listener.getLogger().println("No rebase required. HEAD equals " + targetRepo + "/" + branchName); + } + } + listener.getLogger().println("Pushing HEAD to branch " + branchName + " at repo " + targetRepo); - git.push(remote.getName(), "HEAD:" + branchName); + PushCommand push = git.push().to(remoteURI).ref("HEAD:" + branchName).force(forcePush); + push.execute(); } catch (GitException e) { e.printStackTrace(listener.error("Failed to push branch " + branchName + " to " + targetRepo)); return false; @@ -321,13 +328,17 @@ else if (!tagExists) { final boolean noteReplace = b.getnoteReplace(); try { - RemoteConfig remote = gitSCM.getRepositoryByName(targetRepo); + // Lookup repository with unexpanded name as GitSCM stores them unexpanded + RemoteConfig remote = gitSCM.getRepositoryByName(b.getTargetRepoName()); if (remote == null) { listener.getLogger().println("No repository found for target repo name " + targetRepo); return false; } + // expand environment variables in remote repository + remote = gitSCM.getParamExpandedRepo(environment, remote); + listener.getLogger().println("Adding note to namespace \""+noteNamespace +"\":\n" + noteMsg + "\n******" ); if ( noteReplace ) @@ -335,7 +346,9 @@ else if (!tagExists) { else git.appendNote( noteMsg, noteNamespace ); - git.push(remote.getName(), "refs/notes/*" ); + remoteURI = remote.getURIs().get(0); + PushCommand push = git.push().to(remoteURI).ref("refs/notes/*").force(forcePush); + push.execute(); } catch (GitException e) { e.printStackTrace(listener.error("Failed to add note: \n" + noteMsg + "\n******")); return false; @@ -381,7 +394,10 @@ public String getHelpFile() { * Performs on-the-fly validation on the file mask wildcard. * * I don't think this actually ever gets called, but I'm modernizing it anyway. - * + * @param project project context for evaluation + * @param value string to be evaluated + * @return form validation result + * @throws IOException on input or output error */ public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { @@ -389,15 +405,15 @@ public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryPar } public FormValidation doCheckTagName(@QueryParameter String value) { - return checkFieldNotEmpty(value, "Tag Name"); + return checkFieldNotEmpty(value, Messages.GitPublisher_Check_TagName()); } public FormValidation doCheckBranchName(@QueryParameter String value) { - return checkFieldNotEmpty(value, "Branch Name"); + return checkFieldNotEmpty(value, Messages.GitPublisher_Check_BranchName()); } public FormValidation doCheckNoteMsg(@QueryParameter String value) { - return checkFieldNotEmpty(value, "Note"); + return checkFieldNotEmpty(value, Messages.GitPublisher_Check_Note()); } public FormValidation doCheckRemote( @@ -412,7 +428,7 @@ public FormValidation doCheckRemote( return FormValidation.ok(); FormValidation validation = checkFieldNotEmpty(remote, - "Remote Name"); + Messages.GitPublisher_Check_RemoteName()); if (validation.kind != FormValidation.Kind.OK) return validation; @@ -437,7 +453,7 @@ private FormValidation checkFieldNotEmpty(String value, String field) { value = StringUtils.strip(value); if (value == null || value.equals("")) { - return FormValidation.error(field + " is required."); + return FormValidation.error(Messages.GitPublisher_Check_Required(field)); } return FormValidation.ok(); } @@ -469,6 +485,7 @@ public void setEmptyTargetRepoToOrigin(){ public static final class BranchToPush extends PushConfig { private String branchName; + private boolean rebaseBeforePush; public String getBranchName() { return branchName; @@ -480,6 +497,15 @@ public BranchToPush(String targetRepoName, String branchName) { this.branchName = Util.fixEmptyAndTrim(branchName); } + @DataBoundSetter + public void setRebaseBeforePush(boolean shouldRebase) { + this.rebaseBeforePush = shouldRebase; + } + + public boolean getRebaseBeforePush() { + return rebaseBeforePush; + } + @Extension public static class DescriptorImpl extends Descriptor { @Override diff --git a/src/main/java/hudson/plugins/git/GitRevisionBuildParameters.java b/src/main/java/hudson/plugins/git/GitRevisionBuildParameters.java index fe66c49308..4f378b396c 100644 --- a/src/main/java/hudson/plugins/git/GitRevisionBuildParameters.java +++ b/src/main/java/hudson/plugins/git/GitRevisionBuildParameters.java @@ -54,7 +54,7 @@ public GitRevisionBuildParameters() { @Override public Action getAction(AbstractBuild build, TaskListener listener) { BuildData data = build.getAction(BuildData.class); - if (data == null && Jenkins.getInstance().getPlugin("promoted-builds") != null) { + if (data == null && Jenkins.get().getPlugin("promoted-builds") != null) { if (build instanceof hudson.plugins.promoted_builds.Promotion) { // We are running as a build promotion, so have to retrieve the git scm from target job data = ((hudson.plugins.promoted_builds.Promotion) build).getTarget().getAction(BuildData.class); @@ -65,7 +65,13 @@ public Action getAction(AbstractBuild build, TaskListener listener) { return null; } - return new RevisionParameterAction(data.getLastBuiltRevision(), getCombineQueuedCommits()); + Revision lastBuiltRevision = data.getLastBuiltRevision(); + if (lastBuiltRevision == null) { + listener.getLogger().println("Missing build information. Can't pass the revision to downstream"); + return null; + } + + return new RevisionParameterAction(lastBuiltRevision, getCombineQueuedCommits()); } public boolean getCombineQueuedCommits() { diff --git a/src/main/java/hudson/plugins/git/GitSCM.java b/src/main/java/hudson/plugins/git/GitSCM.java index 1b9720843a..9aa053790b 100644 --- a/src/main/java/hudson/plugins/git/GitSCM.java +++ b/src/main/java/hudson/plugins/git/GitSCM.java @@ -1,61 +1,85 @@ package hudson.plugins.git; +import com.cloudbees.plugins.credentials.CredentialsMatcher; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import com.google.common.collect.Iterables; + import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.*; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import hudson.AbortException; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; import hudson.init.Initializer; -import hudson.matrix.MatrixBuild; -import hudson.matrix.MatrixRun; -import hudson.model.*; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; import hudson.model.Descriptor.FormException; -import hudson.model.Hudson.MasterComputer; +import hudson.model.Items; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Queue; +import hudson.model.Run; +import hudson.model.Saveable; +import hudson.model.TaskListener; +import hudson.model.queue.Tasks; import hudson.plugins.git.browser.GitRepositoryBrowser; -import hudson.plugins.git.extensions.GitClientConflictException; -import hudson.plugins.git.extensions.GitClientType; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; import hudson.plugins.git.extensions.impl.AuthorInChangelog; import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.plugins.git.extensions.impl.ChangelogToBranch; +import hudson.plugins.git.extensions.impl.PathRestriction; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; import hudson.plugins.git.extensions.impl.PreBuildMerge; import hudson.plugins.git.opt.PreBuildMergeOptions; import hudson.plugins.git.util.Build; import hudson.plugins.git.util.*; import hudson.remoting.Channel; -import hudson.scm.*; +import hudson.scm.AbstractScmTagAction; +import hudson.scm.ChangeLogParser; +import hudson.scm.PollingResult; +import hudson.scm.RepositoryBrowser; +import hudson.scm.SCMDescriptor; +import hudson.scm.SCMRevisionState; import hudson.security.ACL; import hudson.tasks.Builder; import hudson.tasks.Publisher; import hudson.triggers.SCMTrigger; import hudson.util.DescribableList; import hudson.util.FormValidation; -import hudson.util.IOException2; -import hudson.util.IOUtils; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; +import jenkins.plugins.git.GitSCMMatrixUtil; import net.sf.json.JSONObject; + +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; import org.jenkinsci.plugins.gitclient.ChangelogCommand; import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.CliGitAPIImpl; import org.jenkinsci.plugins.gitclient.CloneCommand; import org.jenkinsci.plugins.gitclient.FetchCommand; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; -import org.jenkinsci.plugins.gitclient.JGitTool; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.export.Exported; import javax.servlet.ServletException; + import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; @@ -63,16 +87,36 @@ import java.io.Serializable; import java.io.Writer; import java.text.MessageFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import static hudson.Util.*; +import static com.google.common.collect.Lists.newArrayList; import static hudson.init.InitMilestone.JOB_LOADED; import static hudson.init.InitMilestone.PLUGINS_STARTED; +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.browser.GitLab; +import hudson.plugins.git.browser.GithubWeb; import static hudson.scm.PollingResult.*; +import hudson.Util; +import hudson.util.LogTaskListener; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.lang.StringUtils.isBlank; + /** * Git SCM. * @@ -100,18 +144,25 @@ public class GitSCM extends GitSCMBackwardCompatibility { private List branches; private boolean doGenerateSubmoduleConfigurations; - public String gitTool = null; + @CheckForNull + public String gitTool; + @CheckForNull private GitRepositoryBrowser browser; private Collection submoduleCfg; public static final String GIT_BRANCH = "GIT_BRANCH"; + public static final String GIT_LOCAL_BRANCH = "GIT_LOCAL_BRANCH"; + public static final String GIT_CHECKOUT_DIR = "GIT_CHECKOUT_DIR"; public static final String GIT_COMMIT = "GIT_COMMIT"; public static final String GIT_PREVIOUS_COMMIT = "GIT_PREVIOUS_COMMIT"; + public static final String GIT_PREVIOUS_SUCCESSFUL_COMMIT = "GIT_PREVIOUS_SUCCESSFUL_COMMIT"; /** * All the configured extensions attached to this. */ + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="Known non-serializable field") private DescribableList extensions; + @Whitelisted public Collection getSubmoduleCfg() { return submoduleCfg; } @@ -120,24 +171,24 @@ public void setSubmoduleCfg(Collection submoduleCfg) { this.submoduleCfg = submoduleCfg; } - static private List createRepoList(String url) { - List repoList = new ArrayList(); - repoList.add(new UserRemoteConfig(url, null, null, null)); + public static List createRepoList(String url, String credentialsId) { + List repoList = new ArrayList<>(); + repoList.add(new UserRemoteConfig(url, null, null, credentialsId)); return repoList; } /** * A convenience constructor that sets everything to default. * - * @param repositoryUrl + * @param repositoryUrl git repository URL * Repository URL to clone from. */ public GitSCM(String repositoryUrl) { this( - createRepoList(repositoryUrl), + createRepoList(repositoryUrl, null), Collections.singletonList(new BranchSpec("")), false, Collections.emptyList(), - null, null, null); + null, null, Collections.emptyList()); } // @Restricted(NoExternalUse.class) // because this keeps changing @@ -147,18 +198,12 @@ public GitSCM( List branches, Boolean doGenerateSubmoduleConfigurations, Collection submoduleCfg, - GitRepositoryBrowser browser, - String gitTool, + @CheckForNull GitRepositoryBrowser browser, + @CheckForNull String gitTool, List extensions) { // moved from createBranches - if (branches == null) { - branches = new ArrayList(); - } - if (branches.isEmpty()) { - branches.add(new BranchSpec("*/master")); - } - this.branches = branches; + this.branches = isEmpty(branches) ? newArrayList(new BranchSpec("*/master")) : branches; this.userRemoteConfigs = userRemoteConfigs; updateFromUserData(); @@ -174,14 +219,14 @@ public GitSCM( } if (submoduleCfg == null) { - submoduleCfg = new ArrayList(); + submoduleCfg = new ArrayList<>(); } this.submoduleCfg = submoduleCfg; this.configVersion = 2L; this.gitTool = gitTool; - this.extensions = new DescribableList(Saveable.NOOP,Util.fixNull(extensions)); + this.extensions = new DescribableList<>(Saveable.NOOP,Util.fixNull(extensions)); getBuildChooser(); // set the gitSCM field. } @@ -191,14 +236,18 @@ public GitSCM( * * Going forward this is primarily how we'll support esoteric use cases. * - * @since 1.EXTENSION + * @since 2.0 */ + @Whitelisted public DescribableList getExtensions() { return extensions; } private void updateFromUserData() throws GitException { // do what newInstance used to do directly from the request data + if (userRemoteConfigs == null) { + return; /* Prevent NPE when no remote config defined */ + } try { String[] pUrls = new String[userRemoteConfigs.size()]; String[] repoNames = new String[userRemoteConfigs.size()]; @@ -226,11 +275,11 @@ public Object readResolve() throws IOException { if (source != null) { - remoteRepositories = new ArrayList(); - branches = new ArrayList(); + remoteRepositories = new ArrayList<>(); + branches = new ArrayList<>(); doGenerateSubmoduleConfigurations = false; - List rs = new ArrayList(); + List rs = new ArrayList<>(); rs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); remoteRepositories.add(newRemoteConfig("origin", source, rs.toArray(new RefSpec[0]))); if (branch != null) { @@ -252,7 +301,7 @@ public Object readResolve() throws IOException { } if (remoteRepositories != null && userRemoteConfigs == null) { - userRemoteConfigs = new ArrayList(); + userRemoteConfigs = new ArrayList<>(); for(RemoteConfig cfg : remoteRepositories) { // converted as in config.jelly String url = ""; @@ -279,7 +328,7 @@ public Object readResolve() throws IOException { } if (extensions==null) - extensions = new DescribableList(Saveable.NOOP); + extensions = new DescribableList<>(Saveable.NOOP); readBackExtensionsFromLegacy(); @@ -288,9 +337,7 @@ public Object readResolve() throws IOException { if (choosingStrategy.equals(d.getLegacyId())) { try { setBuildChooser(d.clazz.newInstance()); - } catch (InstantiationException e) { - LOGGER.log(Level.WARNING, "Failed to instantiate the build chooser", e); - } catch (IllegalAccessException e) { + } catch (InstantiationException | IllegalAccessException e) { LOGGER.log(Level.WARNING, "Failed to instantiate the build chooser", e); } } @@ -303,15 +350,90 @@ public Object readResolve() throws IOException { } @Override + @Whitelisted public GitRepositoryBrowser getBrowser() { return browser; } + public void setBrowser(GitRepositoryBrowser browser) { + this.browser = browser; + } + + private static final String HOSTNAME_MATCH + = "([\\w\\d[-.]]+)" // hostname + ; + private static final String REPOSITORY_PATH_MATCH + = "/*" // Zero or more slashes as start of repository path + + "(.+?)" // repository path without leading slashes + + "(?:[.]git)?" // optional '.git' suffix + + "/*" // optional trailing '/' + ; + + private static final Pattern[] URL_PATTERNS = { + /* URL style - like https://github.com/jenkinsci/git-plugin */ + Pattern.compile( + "(?:\\w+://)" // protocol (scheme) + + "(?:.+@)?" // optional username/password + + HOSTNAME_MATCH + + "(?:[:][\\d]+)?" // optional port number (only honored by git for ssh:// scheme) + + "/" // separator between hostname and repository path - '/' + + REPOSITORY_PATH_MATCH + ), + /* Alternate ssh style - like git@github.com:jenkinsci/git-plugin */ + Pattern.compile( + "(?:git@)" // required username (only optional if local username is 'git') + + HOSTNAME_MATCH + + ":" // separator between hostname and repository path - ':' + + REPOSITORY_PATH_MATCH + ) + }; + + @Override public RepositoryBrowser guessBrowser() { + Set webUrls = new HashSet<>(); + if (remoteRepositories != null) { + for (RemoteConfig config : remoteRepositories) { + for (URIish uriIsh : config.getURIs()) { + String uri = uriIsh.toString(); + for (Pattern p : URL_PATTERNS) { + Matcher m = p.matcher(uri); + if (m.matches()) { + webUrls.add("https://" + m.group(1) + "/" + m.group(2) + "/"); + } + } + } + } + } + if (webUrls.isEmpty()) { + return null; + } + if (webUrls.size() == 1) { + String url = webUrls.iterator().next(); + if (url.startsWith("https://bitbucket.org/")) { + return new BitbucketWeb(url); + } + if (url.startsWith("https://gitlab.com/")) { + return new GitLab(url, ""); + } + if (url.startsWith("https://github.com/")) { + return new GithubWeb(url); + } + return null; + } + LOGGER.log(Level.INFO, "Multiple browser guess matches for {0}", remoteRepositories); + return null; + } + public boolean isCreateAccountBasedOnEmail() { DescriptorImpl gitDescriptor = getDescriptor(); return (gitDescriptor != null && gitDescriptor.isCreateAccountBasedOnEmail()); } + public boolean isUseExistingAccountWithSameEmail() { + DescriptorImpl gitDescriptor = getDescriptor(); + return (gitDescriptor != null && gitDescriptor.isUseExistingAccountWithSameEmail()); + } + + @Whitelisted public BuildChooser getBuildChooser() { BuildChooser bc; @@ -330,37 +452,66 @@ public void setBuildChooser(BuildChooser buildChooser) throws IOException { } } + @Deprecated + public String getParamLocalBranch(Run build) throws IOException, InterruptedException { + return getParamLocalBranch(build, new LogTaskListener(LOGGER, Level.INFO)); + } + /** * Gets the parameter-expanded effective value in the context of the current build. + * @param build run whose local branch name is returned + * @param listener build log + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @return parameter-expanded local branch name in build. */ - public String getParamLocalBranch(AbstractBuild build) throws IOException, InterruptedException { + public String getParamLocalBranch(Run build, TaskListener listener) throws IOException, InterruptedException { String branch = getLocalBranch(); // substitute build parameters if available - return getParameterString(branch != null ? branch : null, build.getEnvironment()); + return getParameterString(branch != null ? branch : null, build.getEnvironment(listener)); + } + + @Deprecated + public List getParamExpandedRepos(Run build) throws IOException, InterruptedException { + return getParamExpandedRepos(build, new LogTaskListener(LOGGER, Level.INFO)); } /** * Expand parameters in {@link #remoteRepositories} with the parameter values provided in the given build * and return them. * + * @param build run whose local branch name is returned + * @param listener build log + * @throws IOException on input or output error + * @throws InterruptedException when interrupted * @return can be empty but never null. */ - public List getParamExpandedRepos(AbstractBuild build) throws IOException, InterruptedException { - List expandedRepos = new ArrayList(); + public List getParamExpandedRepos(Run build, TaskListener listener) throws IOException, InterruptedException { + List expandedRepos = new ArrayList<>(); - EnvVars env = build.getEnvironment(); + EnvVars env = build.getEnvironment(listener); for (RemoteConfig oldRepo : Util.fixNull(remoteRepositories)) { - expandedRepos.add( - newRemoteConfig( - getParameterString(oldRepo.getName(), env), - getParameterString(oldRepo.getURIs().get(0).toPrivateString(), env), - getRefSpecs(oldRepo, env).toArray(new RefSpec[0]))); + expandedRepos.add(getParamExpandedRepo(env, oldRepo)); } return expandedRepos; } + /** + * Expand Parameters in the supplied remote repository with the parameter values provided in the given environment variables } + * @param env Environment variables with parameter values + * @param remoteRepository Remote repository with parameters + * @return remote repository with expanded parameters + */ + public RemoteConfig getParamExpandedRepo(EnvVars env, RemoteConfig remoteRepository){ + List refSpecs = getRefSpecs(remoteRepository, env); + return newRemoteConfig( + getParameterString(remoteRepository.getName(), env), + getParameterString(remoteRepository.getURIs().get(0).toPrivateString(), env), + refSpecs.toArray(new RefSpec[0])); + } + public RemoteConfig getRepositoryByName(String repoName) { for (RemoteConfig r : getRepositories()) { if (r.getName().equals(repoName)) { @@ -372,29 +523,72 @@ public RemoteConfig getRepositoryByName(String repoName) { } @Exported + @Whitelisted public List getUserRemoteConfigs() { + if (userRemoteConfigs == null) { + /* Prevent NPE when no remote config defined */ + userRemoteConfigs = new ArrayList<>(); + } return Collections.unmodifiableList(userRemoteConfigs); } - @Exported + @Whitelisted public List getRepositories() { // Handle null-value to ensure backwards-compatibility, ie project configuration missing the XML element if (remoteRepositories == null) { - return new ArrayList(); + return new ArrayList<>(); } return remoteRepositories; } + /** + * Derives a local branch name from the remote branch name by removing the + * name of the remote from the remote branch name. + *

+ * Ex. origin/master becomes master + *

+ * Cycles through the list of user remotes looking for a match allowing user + * to configure an alternate (not origin) name for the remote. + * + * @param remoteBranchName branch name whose remote repository name will be removed + * @return a local branch name derived by stripping the remote repository + * name from the {@code remoteBranchName} parameter. If a matching + * remote is not found, the original {@code remoteBranchName} will + * be returned. + */ + public String deriveLocalBranchName(String remoteBranchName) { + // default remoteName is 'origin' used if list of user remote configs is empty. + String remoteName = "origin"; + + for (final UserRemoteConfig remote : getUserRemoteConfigs()) { + remoteName = remote.getName(); + if (remoteName == null || remoteName.isEmpty()) { + remoteName = "origin"; + } + if (remoteBranchName.startsWith(remoteName + "/")) { + // found the remote config associated with remoteBranchName + break; + } + } + + // now strip the remote name and return the resulting local branch name. + String localBranchName = remoteBranchName.replaceFirst("^" + remoteName + "/", ""); + return localBranchName; + } + + @CheckForNull + @Whitelisted public String getGitTool() { return gitTool; } - public static String getParameterString(String original, EnvVars env) { + @NonNull + public static String getParameterString(@CheckForNull String original, @NonNull EnvVars env) { return env.expand(original); } private List getRefSpecs(RemoteConfig repo, EnvVars env) { - List refSpecs = new ArrayList(); + List refSpecs = new ArrayList<>(); for (RefSpec refSpec : repo.getFetchRefSpecs()) { refSpecs.add(new RefSpec(getParameterString(refSpec.toString(), env))); } @@ -405,19 +599,31 @@ private List getRefSpecs(RemoteConfig repo, EnvVars env) { * If the configuration is such that we are tracking just one branch of one repository * return that branch specifier (in the form of something like "origin/master" or a SHA1-hash * - * Otherwise return null. + * Otherwise return [@code null}. */ + @CheckForNull private String getSingleBranch(EnvVars env) { // if we have multiple branches skip to advanced usecase - if (getBranches().size() != 1 || getRepositories().size() != 1) { + if (getBranches().size() != 1) { return null; } - String branch = getBranches().get(0).getName(); - String repository = getRepositories().get(0).getName(); + String repository = null; + + if (getRepositories().size() != 1) { + for (RemoteConfig repo : getRepositories()) { + if (branch.startsWith(repo.getName() + "/")) { + repository = repo.getName(); + break; + } + } + } else { + repository = getRepositories().get(0).getName(); + } + // replace repository wildcard with repository name - if (branch.startsWith("*/")) { + if (branch.startsWith("*/") && repository != null) { branch = repository + branch.substring(1); } @@ -439,33 +645,41 @@ private String getSingleBranch(EnvVars env) { } @Override - public SCMRevisionState calcRevisionsFromBuild(AbstractBuild abstractBuild, Launcher launcher, TaskListener taskListener) throws IOException, InterruptedException { + public SCMRevisionState calcRevisionsFromBuild(Run abstractBuild, FilePath workspace, Launcher launcher, TaskListener taskListener) throws IOException, InterruptedException { return SCMRevisionState.NONE; } @Override public boolean requiresWorkspaceForPolling() { + // TODO would need to use hudson.plugins.git.util.GitUtils.getPollEnvironment + return requiresWorkspaceForPolling(new EnvVars()); + } + + /* Package protected for test access */ + boolean requiresWorkspaceForPolling(EnvVars environment) { for (GitSCMExtension ext : getExtensions()) { if (ext.requiresWorkspaceForPolling()) return true; } - return getSingleBranch(new EnvVars()) == null; + return getSingleBranch(environment) == null; } @Override - protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, final TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { + public PollingResult compareRemoteRevisionWith(Job project, Launcher launcher, FilePath workspace, final TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { try { return compareRemoteRevisionWithImpl( project, launcher, workspace, listener); } catch (GitException e){ - throw new IOException2(e); + throw new IOException(e); } } - private PollingResult compareRemoteRevisionWithImpl(AbstractProject project, Launcher launcher, FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { + public static final Pattern GIT_REF = Pattern.compile("^(refs/[^/]+)/(.+)"); + + private PollingResult compareRemoteRevisionWithImpl(Job project, Launcher launcher, FilePath workspace, final @NonNull TaskListener listener) throws IOException, InterruptedException { // Poll for changes. Are there any unbuilt revisions that Hudson ought to build ? listener.getLogger().println("Using strategy: " + getBuildChooser().getDisplayName()); - final AbstractBuild lastBuild = project.getLastBuild(); + final Run lastBuild = project.getLastBuild(); if (lastBuild == null) { // If we've never been built before, well, gotta build! listener.getLogger().println("[poll] No previous build, so forcing an initial build."); @@ -477,57 +691,99 @@ private PollingResult compareRemoteRevisionWithImpl(AbstractProject projec listener.getLogger().println("[poll] Last Built Revision: " + buildData.lastBuild.revision); } - final String singleBranch = getSingleBranch(lastBuild.getEnvironment()); + final EnvVars pollEnv = project instanceof AbstractProject ? GitUtils.getPollEnvironment((AbstractProject) project, workspace, launcher, listener, false) : lastBuild.getEnvironment(listener); - // fast remote polling needs a single branch and an existing last build - if (!requiresWorkspaceForPolling() && buildData.lastBuild != null && buildData.lastBuild.getMarked() != null) { + final String singleBranch = getSingleBranch(pollEnv); - // FIXME this should not be a specific case, but have BuildChooser tell us if it can poll without workspace. + if (!requiresWorkspaceForPolling(pollEnv)) { - final EnvVars environment = GitUtils.getPollEnvironment(project, workspace, launcher, listener, false); + final EnvVars environment = project instanceof AbstractProject ? GitUtils.getPollEnvironment((AbstractProject) project, workspace, launcher, listener, false) : new EnvVars(); - GitClient git = createClient(listener, environment, project, Jenkins.getInstance(), null); + GitClient git = createClient(listener, environment, project, Jenkins.get(), null); - String gitRepo = getParamExpandedRepos(lastBuild).get(0).getURIs().get(0).toString(); - ObjectId head = git.getHeadRev(gitRepo, getBranches().get(0).getName()); + for (RemoteConfig remoteConfig : getParamExpandedRepos(lastBuild, listener)) { + String remote = remoteConfig.getName(); + List refSpecs = getRefSpecs(remoteConfig, environment); - if (head != null && buildData.lastBuild.getMarked().getSha1().equals(head)) { - return NO_CHANGES; - } else { - return BUILD_NOW; + for (URIish urIish : remoteConfig.getURIs()) { + String gitRepo = urIish.toString(); + Map heads = git.getHeadRev(gitRepo); + if (heads==null || heads.isEmpty()) { + listener.getLogger().println("[poll] Couldn't get remote head revision"); + return BUILD_NOW; + } + + listener.getLogger().println("Found "+ heads.size() +" remote heads on " + urIish); + + Iterator> it = heads.entrySet().iterator(); + while (it.hasNext()) { + String head = it.next().getKey(); + boolean match = false; + for (RefSpec spec : refSpecs) { + if (spec.matchSource(head)) { + match = true; + break; + } + } + if (!match) { + listener.getLogger().println("Ignoring " + head + " as it doesn't match any of the configured refspecs"); + it.remove(); + } + } + + for (BranchSpec branchSpec : getBranches()) { + for (Entry entry : heads.entrySet()) { + final String head = entry.getKey(); + // head is "refs/(heads|tags|whatever)/branchName + + // first, check the a canonical git reference is configured + if (!branchSpec.matches(head, environment)) { + + // convert head `refs/(heads|tags|whatever)/branch` into shortcut notation `remote/branch` + String name; + Matcher matcher = GIT_REF.matcher(head); + if (matcher.matches()) name = remote + head.substring(matcher.group(1).length()); + else name = remote + "/" + head; + + if (!branchSpec.matches(name, environment)) continue; + } + + final ObjectId sha1 = entry.getValue(); + Build built = buildData.getLastBuild(sha1); + if (built != null) { + listener.getLogger().println("[poll] Latest remote head revision on " + head + " is: " + sha1.getName() + " - already built by " + built.getBuildNumber()); + continue; + } + + listener.getLogger().println("[poll] Latest remote head revision on " + head + " is: " + sha1.getName()); + return BUILD_NOW; + } + } + } } + return NO_CHANGES; } - final EnvVars environment = GitUtils.getPollEnvironment(project, workspace, launcher, listener); + final Node node = GitUtils.workspaceToNode(workspace); + final EnvVars environment = project instanceof AbstractProject ? GitUtils.getPollEnvironment((AbstractProject) project, workspace, launcher, listener) : project.getEnvironment(node, listener); FilePath workingDirectory = workingDirectory(project,workspace,environment,listener); // (Re)build if the working directory doesn't exist if (workingDirectory == null || !workingDirectory.exists()) { + listener.getLogger().println("[poll] Working Directory does not exist"); return BUILD_NOW; } - // which node is this workspace from? - // there should be always one match, but just in case we initialize n to a non-null value - Node n = Jenkins.getInstance(); - if (workspace.isRemote()) { - for (Computer c : Jenkins.getInstance().getComputers()) { - if (c.getChannel()==workspace.getChannel()) { - n = c.getNode(); - break; - } - } - } - - GitClient git = createClient(listener, environment, project, n, workingDirectory); + GitClient git = createClient(listener, environment, project, node, workingDirectory); if (git.hasGitRepo()) { // Repo is there - do a fetch listener.getLogger().println("Fetching changes from the remote Git repositories"); // Fetch updates - for (RemoteConfig remoteRepository : getParamExpandedRepos(lastBuild)) { - fetchFrom(git, listener, remoteRepository); + for (RemoteConfig remoteRepository : getParamExpandedRepos(lastBuild, listener)) { + fetchFrom(git, null, listener, remoteRepository); } listener.getLogger().println("Polling for changes in"); @@ -551,14 +807,26 @@ private PollingResult compareRemoteRevisionWithImpl(AbstractProject projec /** * Allows {@link Builder}s and {@link Publisher}s to access a configured {@link GitClient} object to * perform additional git operations. + * @param listener build log + * @param environment environment variables to be used + * @param build run context for the returned GitClient + * @param workspace client workspace + * @return git client for additional git operations + * @throws IOException on input or output error + * @throws InterruptedException when interrupted */ - public GitClient createClient(BuildListener listener, EnvVars environment, AbstractBuild build) throws IOException, InterruptedException { - FilePath ws = workingDirectory(build.getProject(), build.getWorkspace(), environment, listener); - ws.mkdirs(); // ensure it exists - return createClient(listener,environment, build.getParent(), build.getBuiltOn(), ws); + @NonNull + public GitClient createClient(TaskListener listener, EnvVars environment, Run build, FilePath workspace) throws IOException, InterruptedException { + FilePath ws = workingDirectory(build.getParent(), workspace, environment, listener); + /* ws will be null if the node which ran the build is offline */ + if (ws != null) { + ws.mkdirs(); // ensure it exists + } + return createClient(listener,environment, build.getParent(), GitUtils.workspaceToNode(workspace), ws); } - /*package*/ GitClient createClient(TaskListener listener, EnvVars environment, AbstractProject project, Node n, FilePath ws) throws IOException, InterruptedException { + @NonNull + /*package*/ GitClient createClient(TaskListener listener, EnvVars environment, Job project, Node n, FilePath ws) throws IOException, InterruptedException { String gitExe = getGitExe(n, listener); Git git = Git.with(listener, environment).in(ws).using(gitExe); @@ -569,16 +837,30 @@ public GitClient createClient(BuildListener listener, EnvVars environment, Abstr } for (UserRemoteConfig uc : getUserRemoteConfigs()) { - if (uc.getCredentialsId() != null) { - String url = uc.getUrl(); - StandardUsernameCredentials credentials = CredentialsMatchers - .firstOrNull( - CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, project, - ACL.SYSTEM, URIRequirementBuilder.fromUri(url).build()), - CredentialsMatchers.allOf(CredentialsMatchers.withId(uc.getCredentialsId()), - GitClient.CREDENTIALS_MATCHER)); + String ucCredentialsId = uc.getCredentialsId(); + if (ucCredentialsId == null) { + listener.getLogger().println("No credentials specified"); + } else { + String url = getParameterString(uc.getUrl(), environment); + List urlCredentials = CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, + project, + project instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task)project) + : ACL.SYSTEM, + URIRequirementBuilder.fromUri(url).build() + ); + CredentialsMatcher ucMatcher = CredentialsMatchers.withId(ucCredentialsId); + CredentialsMatcher idMatcher = CredentialsMatchers.allOf(ucMatcher, GitClient.CREDENTIALS_MATCHER); + StandardUsernameCredentials credentials = CredentialsMatchers.firstOrNull(urlCredentials, idMatcher); if (credentials != null) { c.addCredentials(url, credentials); + listener.getLogger().println(format("using credential %s", credentials.getId())); + if (project != null && project.getLastBuild() != null) { + CredentialsProvider.track(project.getLastBuild(), credentials); + } + } else { + listener.getLogger().println(format("Warning: CredentialId \"%s\" could not be found.", ucCredentialsId)); } } } @@ -587,6 +869,7 @@ public GitClient createClient(BuildListener listener, EnvVars environment, Abstr return c; } + @NonNull private BuildData fixNull(BuildData bd) { return bd != null ? bd : new BuildData(getScmName(), getUserRemoteConfigs()) /*dummy*/; } @@ -594,13 +877,15 @@ private BuildData fixNull(BuildData bd) { /** * Fetch information from a particular remote repository. * - * @param git - * @param listener - * @param remoteRepository - * @throws InterruptedException - * @throws IOException + * @param git git client + * @param run run context if it's running for build + * @param listener build log + * @param remoteRepository remote git repository + * @throws InterruptedException when interrupted + * @throws IOException on input or output error */ private void fetchFrom(GitClient git, + @CheckForNull Run run, TaskListener listener, RemoteConfig remoteRepository) throws InterruptedException, IOException { @@ -616,7 +901,7 @@ private void fetchFrom(GitClient git, FetchCommand fetch = git.fetch_().from(url, remoteRepository.getFetchRefSpecs()); for (GitSCMExtension extension : extensions) { - extension.decorateFetchCommand(this, git, listener, fetch); + extension.decorateFetchCommand(this, run, git, listener, fetch); } fetch.execute(); } catch (GitException ex) { @@ -632,7 +917,7 @@ private RemoteConfig newRemoteConfig(String name, String refUrl, RefSpec... refS // Make up a repo config from the request parameters repoConfig.setString("remote", name, "url", refUrl); - List str = new ArrayList(); + List str = new ArrayList<>(); if(refSpec != null && refSpec.length > 0) for (RefSpec rs: refSpec) str.add(rs.toString()); @@ -644,14 +929,9 @@ private RemoteConfig newRemoteConfig(String name, String refUrl, RefSpec... refS } } + @CheckForNull public GitTool resolveGitTool(TaskListener listener) { - if (gitTool == null) return GitTool.getDefaultInstallation(); - GitTool git = Jenkins.getInstance().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallation(gitTool); - if (git == null) { - listener.getLogger().println("selected Git installation does not exists. Using Default"); - git = GitTool.getDefaultInstallation(); - } - return git; + return GitUtils.resolveGitTool(gitTool, listener); } public String getGitExe(Node builtOn, TaskListener listener) { @@ -660,71 +940,41 @@ public String getGitExe(Node builtOn, TaskListener listener) { /** * Exposing so that we can get this from GitPublisher. + * @param builtOn node where build was performed + * @param env environment variables used in the build + * @param listener build log + * @return git exe for builtOn node, often "Default" or "jgit" */ public String getGitExe(Node builtOn, EnvVars env, TaskListener listener) { - - GitClientType client = GitClientType.ANY; - for (GitSCMExtension ext : extensions) { - try { - client = client.combine(ext.getRequiredClient()); - } catch (GitClientConflictException e) { - throw new RuntimeException(ext.getDescriptor().getDisplayName() + " extended Git behavior is incompatible with other behaviors"); - } - } - if (client == GitClientType.JGIT) return JGitTool.MAGIC_EXENAME; - - GitTool tool = resolveGitTool(listener); - if (builtOn != null) { - try { - tool = tool.forNode(builtOn, listener); - } catch (IOException e) { - listener.getLogger().println("Failed to get git executable"); - } catch (InterruptedException e) { - listener.getLogger().println("Failed to get git executable"); - } - } - if (env != null) { - tool = tool.forEnvironment(env); + GitTool tool = GitUtils.resolveGitTool(gitTool, builtOn, env, listener); + if(tool == null) { + return null; } - return tool.getGitExe(); } - /** - * Web-bound method to let people look up a build by their SHA1 commit. - */ - public AbstractBuild getBySHA1(String sha1) { - AbstractProject p = Stapler.getCurrentRequest().findAncestorObject(AbstractProject.class); - for (AbstractBuild b : p.getBuilds()) { - BuildData d = b.getAction(BuildData.class); - if (d!=null && d.lastBuild!=null) { - Build lb = d.lastBuild; - if (lb.isFor(sha1)) return b; - } - } - return null; - } - /*package*/ static class BuildChooserContextImpl implements BuildChooserContext, Serializable { - final AbstractProject project; - final AbstractBuild build; + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="known non-serializable field") + final Job project; + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="known non-serializable field") + final Run build; final EnvVars environment; - BuildChooserContextImpl(AbstractProject project, AbstractBuild build, EnvVars environment) { + BuildChooserContextImpl(Job project, Run build, EnvVars environment) { this.project = project; this.build = build; this.environment = environment; } - public T actOnBuild(ContextCallable, T> callable) throws IOException, InterruptedException { - return callable.invoke(build,Hudson.MasterComputer.localChannel); + public T actOnBuild(ContextCallable, T> callable) throws IOException, InterruptedException { + return callable.invoke(build, FilePath.localChannel); } - public T actOnProject(ContextCallable, T> callable) throws IOException, InterruptedException { - return callable.invoke(project, MasterComputer.localChannel); + public T actOnProject(ContextCallable, T> callable) throws IOException, InterruptedException { + return callable.invoke(project, FilePath.localChannel); } - public AbstractBuild getBuild() { + public Run getBuild() { return build; } @@ -734,15 +984,15 @@ public EnvVars getEnvironment() { private Object writeReplace() { return Channel.current().export(BuildChooserContext.class,new BuildChooserContext() { - public T actOnBuild(ContextCallable, T> callable) throws IOException, InterruptedException { + public T actOnBuild(ContextCallable, T> callable) throws IOException, InterruptedException { return callable.invoke(build,Channel.current()); } - public T actOnProject(ContextCallable, T> callable) throws IOException, InterruptedException { + public T actOnProject(ContextCallable, T> callable) throws IOException, InterruptedException { return callable.invoke(project,Channel.current()); } - public AbstractBuild getBuild() { + public Run getBuild() { return build; } @@ -763,59 +1013,76 @@ public EnvVars getEnvironment() { * messed up (such as HEAD pointing to a random branch.) It is expected that this method brings it back * to the predictable clean state by the time this method returns. */ - private @NonNull Build determineRevisionToBuild(final AbstractBuild build, - final BuildData buildData, + private @NonNull Build determineRevisionToBuild(final Run build, + final @NonNull BuildData buildData, final EnvVars environment, - final GitClient git, - final BuildListener listener) throws IOException, InterruptedException { + final @NonNull GitClient git, + final @NonNull TaskListener listener) throws IOException, InterruptedException { PrintStream log = listener.getLogger(); + Collection candidates = Collections.emptyList(); + final BuildChooserContext context = new BuildChooserContextImpl(build.getParent(), build, environment); + getBuildChooser().prepareWorkingTree(git, listener, context); - // every MatrixRun should build the exact same commit ID - if (build instanceof MatrixRun) { - MatrixBuild parentBuild = ((MatrixRun) build).getParentBuild(); - if (parentBuild != null) { - BuildData parentBuildData = getBuildData(parentBuild); - if (parentBuildData != null) { - Build lastBuild = parentBuildData.lastBuild; - if (lastBuild!=null) - return lastBuild; - } - } + if (build.getClass().getName().equals("hudson.matrix.MatrixRun")) { + candidates = GitSCMMatrixUtil.populateCandidatesFromRootBuild((AbstractBuild) build, this); } // parameter forcing the commit ID to build - final RevisionParameterAction rpa = build.getAction(RevisionParameterAction.class); - if (rpa != null) - return new Build(rpa.toRevision(git), build.getNumber(), null); + if (candidates.isEmpty() ) { + final RevisionParameterAction rpa = build.getAction(RevisionParameterAction.class); + if (rpa != null) { + // in case the checkout is due to a commit notification on a + // multiple scm configuration, it should be verified if the triggering repo remote + // matches current repo remote to avoid JENKINS-26587 + if (rpa.canOriginateFrom(this.getRepositories())) { + candidates = Collections.singleton(rpa.toRevision(git)); + } else { + log.println("skipping resolution of commit " + rpa.commit + ", since it originates from another repository"); + } + } + } - final String singleBranch = environment.expand( getSingleBranch(environment) ); + if (candidates.isEmpty() ) { + final String singleBranch = environment.expand( getSingleBranch(environment) ); - final BuildChooserContext context = new BuildChooserContextImpl(build.getProject(), build, environment); - Collection candidates = getBuildChooser().getCandidateRevisions( - false, singleBranch, git, listener, buildData, context); + candidates = getBuildChooser().getCandidateRevisions( + false, singleBranch, git, listener, buildData, context); + } - if (candidates.size() == 0) { + if (candidates.isEmpty()) { // getBuildCandidates should make the last item the last build, so a re-build // will build the last built thing. throw new AbortException("Couldn't find any revision to build. Verify the repository and branch configuration for this job."); } + Revision marked = candidates.iterator().next(); + Revision rev = marked; + // Modify the revision based on extensions + for (GitSCMExtension ext : extensions) { + rev = ext.decorateRevisionToBuild(this,build,git,listener,marked,rev); + } + Build revToBuild = new Build(marked, rev, build.getNumber(), null); + buildData.saveBuild(revToBuild); + + if (buildData.getBuildsByBranchName().size() >= 100) { + log.println("JENKINS-19022: warning: possible memory leak due to Git plugin usage; see: https://wiki.jenkins.io/display/JENKINS/Remove+Git+Plugin+BuildsByBranch+BuildData"); + } + if (candidates.size() > 1) { log.println("Multiple candidate revisions"); - AbstractProject project = build.getProject(); - if (!project.isDisabled()) { - log.println("Scheduling another build to catch up with " + project.getFullDisplayName()); - if (!project.scheduleBuild(0, new SCMTrigger.SCMTriggerCause())) { - log.println("WARNING: multiple candidate revisions, but unable to schedule build of " + project.getFullDisplayName()); + Job job = build.getParent(); + if (job instanceof AbstractProject) { + AbstractProject project = (AbstractProject) job; + if (!project.isDisabled()) { + log.println("Scheduling another build to catch up with " + project.getFullDisplayName()); + if (!project.scheduleBuild(0, new SCMTrigger.SCMTriggerCause("This build was triggered by build " + + build.getNumber() + " because more than one build candidate was found."))) { + log.println("WARNING: multiple candidate revisions, but unable to schedule build of " + project.getFullDisplayName()); + } } } } - Revision rev = candidates.iterator().next(); - Revision marked = rev; - for (GitSCMExtension ext : extensions) { - rev = ext.decorateRevisionToBuild(this,build,git,listener,rev); - } - return new Build(marked, rev, build.getNumber(), null); + return revToBuild; } /** @@ -823,10 +1090,10 @@ public EnvVars getEnvironment() { * * By the end of this method, remote refs are updated to include all the commits found in the remote servers. */ - private void retrieveChanges(AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException { + private void retrieveChanges(Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException { final PrintStream log = listener.getLogger(); - List repos = getParamExpandedRepos(build); + List repos = getParamExpandedRepos(build, listener); if (repos.isEmpty()) return; // defensive check even though this is an invalid configuration if (git.hasGitRepo()) { @@ -846,32 +1113,44 @@ private void retrieveChanges(AbstractBuild build, GitClient git, BuildListener l } cmd.execute(); } catch (GitException ex) { - ex.printStackTrace(listener.error("Error cloning remote repo '%s'", rc.getName())); - throw new AbortException(); + ex.printStackTrace(listener.error("Error cloning remote repo '" + rc.getName() + "'")); + throw new AbortException("Error cloning remote repo '" + rc.getName() + "'"); } } for (RemoteConfig remoteRepository : repos) { - fetchFrom(git, listener, remoteRepository); + try { + fetchFrom(git, build, listener, remoteRepository); + } catch (GitException ex) { + /* Allow retry by throwing AbortException instead of + * GitException. See JENKINS-20531. */ + ex.printStackTrace(listener.error("Error fetching remote repo '" + remoteRepository.getName() + "'")); + throw new AbortException("Error fetching remote repo '" + remoteRepository.getName() + "'"); + } } } @Override - public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) + public void checkout(Run build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException { if (VERBOSE) - listener.getLogger().println("Using strategy: " + getBuildChooser().getDisplayName()); + listener.getLogger().println("Using checkout strategy: " + getBuildChooser().getDisplayName()); BuildData previousBuildData = getBuildData(build.getPreviousBuild()); // read only BuildData buildData = copyBuildData(build.getPreviousBuild()); - build.addAction(buildData); + if (VERBOSE && buildData.lastBuild != null) { listener.getLogger().println("Last Built Revision: " + buildData.lastBuild.revision); } EnvVars environment = build.getEnvironment(listener); - GitClient git = createClient(listener,environment,build); + GitClient git = createClient(listener, environment, build, workspace); + + if (launcher instanceof Launcher.DecoratedLauncher) { + // We cannot check for git instanceof CliGitAPIImpl vs. JGitAPIImpl here since (when running on an agent) we will actually have a RemoteGitImpl which is opaque. + listener.getLogger().println("Warning: JENKINS-30600: special launcher " + launcher + " will be ignored (a typical symptom is the Git executable not being run inside a designated container)"); + } for (GitSCMExtension ext : extensions) { ext.beforeCheckout(this, build, git, listener); @@ -880,36 +1159,93 @@ public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspa retrieveChanges(build, git, listener); Build revToBuild = determineRevisionToBuild(build, buildData, environment, git, listener); + // Track whether we're trying to add a duplicate BuildData, now that it's been updated with + // revision info for this build etc. The default assumption is that it's a duplicate. + boolean buildDataAlreadyPresent = false; + List actions = build.getActions(BuildData.class); + for (BuildData d: actions) { + if (d.similarTo(buildData)) { + buildDataAlreadyPresent = true; + break; + } + } + if (!actions.isEmpty()) { + buildData.setIndex(actions.size()+1); + } + + // If the BuildData is not already attached to this build, add it to the build and mark that + // it wasn't already present, so that we add the GitTagAction and changelog after the checkout + // finishes. + if (!buildDataAlreadyPresent) { + build.addAction(buildData); + } + environment.put(GIT_COMMIT, revToBuild.revision.getSha1String()); - Branch branch = Iterables.getFirst(revToBuild.revision.getBranches(),null); - if (branch!=null) // null for a detached HEAD - environment.put(GIT_BRANCH, branch.getName()); + Branch localBranch = Iterables.getFirst(revToBuild.revision.getBranches(),null); + String localBranchName = getParamLocalBranch(build, listener); + if (localBranch != null && localBranch.getName() != null) { // null for a detached HEAD + String remoteBranchName = getBranchName(localBranch); + environment.put(GIT_BRANCH, remoteBranchName); + + LocalBranch lb = getExtensions().get(LocalBranch.class); + if (lb != null) { + String lbn = lb.getLocalBranch(); + if (lbn == null || lbn.equals("**")) { + // local branch is configured with empty value or "**" so use remote branch name for checkout + localBranchName = deriveLocalBranchName(remoteBranchName); + } + environment.put(GIT_LOCAL_BRANCH, localBranchName); + } + } listener.getLogger().println("Checking out " + revToBuild.revision); - CheckoutCommand checkoutCommand = git.checkout().branch(getParamLocalBranch(build)).ref(revToBuild.revision.getSha1String()).deleteBranchIfExist(true); + CheckoutCommand checkoutCommand = git.checkout().branch(localBranchName).ref(revToBuild.revision.getSha1String()).deleteBranchIfExist(true); for (GitSCMExtension ext : this.getExtensions()) { ext.decorateCheckoutCommand(this, build, git, listener, checkoutCommand); } try { checkoutCommand.execute(); - } catch(GitLockFailedException e) { + } catch (GitLockFailedException e) { // Rethrow IOException so the retry will be able to catch it throw new IOException("Could not checkout " + revToBuild.revision.getSha1String(), e); } - buildData.saveBuild(revToBuild); - build.addAction(new GitTagAction(build, buildData)); + // Needs to be after the checkout so that revToBuild is in the workspace + try { + printCommitMessageToLog(listener, git, revToBuild); + } catch (GitException ge) { + listener.getLogger().println("Exception logging commit message for " + revToBuild + ": " + ge.getMessage()); + } - computeChangeLog(git, revToBuild.revision, listener, previousBuildData, new FilePath(changelogFile), - new BuildChooserContextImpl(build.getProject(), build, environment)); + // Don't add the tag and changelog if we've already processed this BuildData before. + if (!buildDataAlreadyPresent) { + if (build.getActions(AbstractScmTagAction.class).isEmpty()) { + // only add the tag action if we can be unique as AbstractScmTagAction has a fixed UrlName + // so only one of the actions is addressable by users + build.addAction(new GitTagAction(build, workspace, revToBuild.revision)); + } + + if (changelogFile != null) { + computeChangeLog(git, revToBuild.revision, listener, previousBuildData, new FilePath(changelogFile), + new BuildChooserContextImpl(build.getParent(), build, environment)); + } + } for (GitSCMExtension ext : extensions) { ext.onCheckoutCompleted(this, build, git,listener); } + } - return true; + private void printCommitMessageToLog(TaskListener listener, GitClient git, final Build revToBuild) + throws IOException { + try { + RevCommit commit = git.withRepository(new RevCommitRepositoryCallback(revToBuild)); + listener.getLogger().println("Commit message: \"" + commit.getShortMessage() + "\""); + } catch (InterruptedException | MissingObjectException e) { + e.printStackTrace(listener.error("Unable to retrieve commit message")); + } } /** @@ -929,7 +1265,7 @@ public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspa * * *

- * If Jenkin built B1, C1, B2, C3 in that order, then one'd prefer that the changelog of B2 only shows + * If Jenkins built B1, C1, B2, C3 in that order, then one'd prefer that the changelog of B2 only shows * just B1..B2, not C1..B2. To do this, we attribute every build to specific branches, and when we say * "since the previous build", what we really mean is "since the last build that built the same branch". * @@ -954,21 +1290,27 @@ public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspa * Information that captures what we did during the last build. We need this for changelog, * or else we won't know where to stop. */ - private void computeChangeLog(GitClient git, Revision revToBuild, BuildListener listener, BuildData previousBuildData, FilePath changelogFile, BuildChooserContext context) throws IOException, InterruptedException { - Writer out = new OutputStreamWriter(changelogFile.write(),"UTF-8"); - + private void computeChangeLog(GitClient git, Revision revToBuild, TaskListener listener, BuildData previousBuildData, FilePath changelogFile, BuildChooserContext context) throws IOException, InterruptedException { boolean executed = false; ChangelogCommand changelog = git.changelog(); changelog.includes(revToBuild.getSha1()); - try { + try (Writer out = new OutputStreamWriter(changelogFile.write(),"UTF-8")) { boolean exclusion = false; - for (Branch b : revToBuild.getBranches()) { - Build lastRevWas = getBuildChooser().prevBuildForChangelog(b.getName(), previousBuildData, git, context); - if (lastRevWas != null && git.isCommitInRepo(lastRevWas.getSHA1())) { - changelog.excludes(lastRevWas.getSHA1()); - exclusion = true; + ChangelogToBranch changelogToBranch = getExtensions().get(ChangelogToBranch.class); + if (changelogToBranch != null) { + listener.getLogger().println("Using 'Changelog to branch' strategy."); + changelog.excludes(changelogToBranch.getOptions().getRef()); + exclusion = true; + } else { + for (Branch b : revToBuild.getBranches()) { + Build lastRevWas = getBuildChooser().prevBuildForChangelog(b.getName(), previousBuildData, git, context); + if (lastRevWas != null && lastRevWas.revision != null && git.isCommitInRepo(lastRevWas.getSHA1())) { + changelog.excludes(lastRevWas.getSHA1()); + exclusion = true; + } } } + if (!exclusion) { // this is the first time we are building this branch, so there's no base line to compare against. // if we force the changelog, it'll contain all the changes in the repo, which is not what we want. @@ -981,28 +1323,60 @@ private void computeChangeLog(GitClient git, Revision revToBuild, BuildListener ge.printStackTrace(listener.error("Unable to retrieve changeset")); } finally { if (!executed) changelog.abort(); - IOUtils.closeQuietly(out); } } - public void buildEnvVars(AbstractBuild build, java.util.Map env) { - super.buildEnvVars(build, env); + public void buildEnvVars(AbstractBuild build, Map env) { + buildEnvironment(build, env); + } + + @Override + public void buildEnvironment(Run build, java.util.Map env) { Revision rev = fixNull(getBuildData(build)).getLastBuiltRevision(); if (rev!=null) { Branch branch = Iterables.getFirst(rev.getBranches(), null); - if (branch!=null) { - env.put(GIT_BRANCH, branch.getName()); + if (branch!=null && branch.getName()!=null) { + String remoteBranchName = getBranchName(branch); + env.put(GIT_BRANCH, remoteBranchName); + + // TODO this is unmodular; should rather override LocalBranch.populateEnvironmentVariables + LocalBranch lb = getExtensions().get(LocalBranch.class); + if (lb != null) { + // Set GIT_LOCAL_BRANCH variable from the LocalBranch extension + String localBranchName = lb.getLocalBranch(); + if (localBranchName == null || localBranchName.equals("**")) { + // local branch is configured with empty value or "**" so use remote branch name for checkout + localBranchName = deriveLocalBranchName(remoteBranchName); + } + env.put(GIT_LOCAL_BRANCH, localBranchName); + } + RelativeTargetDirectory rtd = getExtensions().get(RelativeTargetDirectory.class); + if (rtd != null) { + String localRelativeTargetDir = rtd.getRelativeTargetDir(); + if ( localRelativeTargetDir == null ){ + localRelativeTargetDir = ""; + } + env.put(GIT_CHECKOUT_DIR, localRelativeTargetDir); + } String prevCommit = getLastBuiltCommitOfBranch(build, branch); if (prevCommit != null) { env.put(GIT_PREVIOUS_COMMIT, prevCommit); } + + String prevSuccessfulCommit = getLastSuccessfulBuiltCommitOfBranch(build, branch); + if (prevSuccessfulCommit != null) { + env.put(GIT_PREVIOUS_SUCCESSFUL_COMMIT, prevSuccessfulCommit); + } } - env.put(GIT_COMMIT, fixEmpty(rev.getSha1String())); + String sha1 = Util.fixEmpty(rev.getSha1String()); + if (sha1 != null && !sha1.isEmpty()) { + env.put(GIT_COMMIT, sha1); + } } - + if (userRemoteConfigs.size()==1){ env.put("GIT_URL", userRemoteConfigs.get(0).getUrl()); } else { @@ -1010,7 +1384,7 @@ public void buildEnvVars(AbstractBuild build, java.util.Map build, java.util.Map build, Branch branch) { + private String getBranchName(Branch branch) + { + String name = branch.getName(); + if(name.startsWith("refs/remotes/")) { + //Restore expected previous behaviour + name = name.substring("refs/remotes/".length()); + } + return name; + } + + private String getLastBuiltCommitOfBranch(Run build, Branch branch) { String prevCommit = null; if (build.getPreviousBuiltBuild() != null) { final Build lastBuildOfBranch = fixNull(getBuildData(build.getPreviousBuiltBuild())).getLastBuildOfBranch(branch.getName()); @@ -1033,9 +1417,30 @@ private String getLastBuiltCommitOfBranch(AbstractBuild build, Branch bran return prevCommit; } + private String getLastSuccessfulBuiltCommitOfBranch(Run build, Branch branch) { + String prevCommit = null; + if (build.getPreviousSuccessfulBuild() != null) { + final Build lastSuccessfulBuildOfBranch = fixNull(getBuildData(build.getPreviousSuccessfulBuild())).getLastBuildOfBranch(branch.getName()); + if (lastSuccessfulBuildOfBranch != null) { + Revision previousRev = lastSuccessfulBuildOfBranch.getRevision(); + if (previousRev != null) { + prevCommit = previousRev.getSha1String(); + } + } + } + + return prevCommit; + } + @Override public ChangeLogParser createChangeLogParser() { - return new GitChangeLogParser(getExtensions().get(AuthorInChangelog.class)!=null); + try { + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(gitTool).getClient(); + return new GitChangeLogParser(gitClient, getExtensions().get(AuthorInChangelog.class) != null); + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Git client using '" + gitTool + "' changelog parser failed, using deprecated changelog parser", e); + } + return new GitChangeLogParser(null, getExtensions().get(AuthorInChangelog.class) != null); } @Extension @@ -1045,23 +1450,37 @@ public static final class DescriptorImpl extends SCMDescriptor { private String globalConfigName; private String globalConfigEmail; private boolean createAccountBasedOnEmail; + private boolean useExistingAccountWithSameEmail; // private GitClientType defaultClientType = GitClientType.GITCLI; + private boolean showEntireCommitSummaryInChanges; public DescriptorImpl() { super(GitSCM.class, GitRepositoryBrowser.class); load(); } + public boolean isShowEntireCommitSummaryInChanges() { + return showEntireCommitSummaryInChanges; + } + + public void setShowEntireCommitSummaryInChanges(boolean showEntireCommitSummaryInChanges) { + this.showEntireCommitSummaryInChanges = showEntireCommitSummaryInChanges; + } + public String getDisplayName() { return "Git"; } + @Override public boolean isApplicable(Job project) { + return true; + } + public List getExtensionDescriptors() { return GitSCMExtensionDescriptor.all(); } public boolean showGitToolOptions() { - return Jenkins.getInstance().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallations().length>1; + return Jenkins.get().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallations().length>1; } /** @@ -1069,7 +1488,7 @@ public boolean showGitToolOptions() { * @return list of available git tools */ public List getGitTools() { - GitTool[] gitToolInstallations = Hudson.getInstance().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallations(); + GitTool[] gitToolInstallations = Jenkins.get().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallations(); return Arrays.asList(gitToolInstallations); } @@ -1085,6 +1504,7 @@ public ListBoxModel doFillGitToolItems() { * Path to git executable. * @deprecated * @see GitTool + * @return git executable */ @Deprecated public String getGitExe() { @@ -1093,22 +1513,32 @@ public String getGitExe() { /** * Global setting to be used in call to "git config user.name". + * @return user.name value */ public String getGlobalConfigName() { - return fixEmptyAndTrim(globalConfigName); + return Util.fixEmptyAndTrim(globalConfigName); } + /** + * Global setting to be used in call to "git config user.name". + * @param globalConfigName user.name value to be assigned + */ public void setGlobalConfigName(String globalConfigName) { this.globalConfigName = globalConfigName; } /** * Global setting to be used in call to "git config user.email". + * @return user.email value */ public String getGlobalConfigEmail() { - return fixEmptyAndTrim(globalConfigEmail); + return Util.fixEmptyAndTrim(globalConfigEmail); } + /** + * Global setting to be used in call to "git config user.email". + * @param globalConfigEmail user.email value to be assigned + */ public void setGlobalConfigEmail(String globalConfigEmail) { this.globalConfigEmail = globalConfigEmail; } @@ -1121,9 +1551,18 @@ public void setCreateAccountBasedOnEmail(boolean createAccountBasedOnEmail) { this.createAccountBasedOnEmail = createAccountBasedOnEmail; } + public boolean isUseExistingAccountWithSameEmail() { + return useExistingAccountWithSameEmail; + } + + public void setUseExistingAccountWithSameEmail(boolean useExistingAccountWithSameEmail) { + this.useExistingAccountWithSameEmail = useExistingAccountWithSameEmail; + } + /** * Old configuration of git executable - exposed so that we can * migrate this setting to GitTool without deprecation warnings. + * @return git executable */ public String getOldGitExe() { return gitExe; @@ -1132,7 +1571,7 @@ public String getOldGitExe() { /** * Determine the browser from the scmData contained in the {@link StaplerRequest}. * - * @param scmData + * @param scmData data read for SCM browser * @return browser based on request scmData */ private GitRepositoryBrowser getBrowserFromRequest(final StaplerRequest req, final JSONObject scmData) { @@ -1168,7 +1607,7 @@ public static List createRepositoryConfigurations(String[] urls, } repoConfig.setString("remote", name, "url", url); - repoConfig.setStringList("remote", name, "fetch", new ArrayList(Arrays.asList(refs[i].split("\\s+")))); + repoConfig.setStringList("remote", name, "fetch", new ArrayList<>(Arrays.asList(refs[i].split("\\s+")))); } try { @@ -1202,6 +1641,7 @@ public static PreBuildMergeOptions createMergeOptions(UserMergeOptions mergeOpti mergeOptions.setMergeRemote(mergeRemote); mergeOptions.setMergeTarget(mergeOptionsBean.getMergeTarget()); mergeOptions.setMergeStrategy(mergeOptionsBean.getMergeStrategy()); + mergeOptions.setFastForwardMode(mergeOptionsBean.getFastForwardMode()); } return mergeOptions; @@ -1236,6 +1676,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc /** * Fill in the environment variables for launching git + * @param env base environment variables */ public void populateEnvironmentVariables(Map env) { String name = getGlobalConfigName(); @@ -1261,17 +1702,35 @@ public void populateEnvironmentVariables(Map env) { private static final long serialVersionUID = 1L; + @Whitelisted public boolean isDoGenerateSubmoduleConfigurations() { return this.doGenerateSubmoduleConfigurations; } @Exported + @Whitelisted public List getBranches() { return branches; } + @Override public String getKey() { + String name = getScmName(); + if (name != null) { + return name; + } + StringBuilder b = new StringBuilder("git"); + for (RemoteConfig cfg : getRepositories()) { + for (URIish uri : cfg.getURIs()) { + b.append(' ').append(uri.toString()); + } + } + return b.toString(); + } + /** - * Use {@link PreBuildMerge}. + * @deprecated Use {@link PreBuildMerge}. + * @return pre-build merge options + * @throws FormException on form error */ @Exported @Deprecated @@ -1290,6 +1749,9 @@ private boolean isRelevantBuildData(BuildData bd) { /** * @deprecated + * @param build run whose build data is returned + * @param clone true if returned build data should be copied rather than referenced + * @return build data for build run */ public BuildData getBuildData(Run build, boolean clone) { return clone ? copyBuildData(build) : getBuildData(build); @@ -1298,20 +1760,25 @@ public BuildData getBuildData(Run build, boolean clone) { /** * Like {@link #getBuildData(Run)}, but copy the data into a new object, * which is used as the first step for updating the data for the next build. + * @param build run whose BuildData is returned + * @return copy of build data for build */ public BuildData copyBuildData(Run build) { BuildData base = getBuildData(build); if (base==null) return new BuildData(getScmName(), getUserRemoteConfigs()); - else - return base.clone(); + else { + BuildData buildData = base.clone(); + buildData.setScmName(getScmName()); + return buildData; + } } /** * Find the build log (BuildData) recorded with the last build that completed. BuildData * may not be recorded if an exception occurs in the plugin logic. * - * @param build + * @param build run whose build data is returned * @return the last recorded build data */ public @CheckForNull BuildData getBuildData(Run build) { @@ -1337,10 +1804,15 @@ public BuildData copyBuildData(Run build) { * Given the workspace, gets the working directory, which will be the workspace * if no relative target dir is specified. Otherwise, it'll be "workspace/relativeTargetDir". * - * @param workspace + * @param context job context for working directory + * @param workspace initial FilePath of job workspace + * @param environment environment variables used in job context + * @param listener build log * @return working directory or null if workspace is null + * @throws IOException on input or output error + * @throws InterruptedException when interrupted */ - protected FilePath workingDirectory(AbstractProject context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException { + protected FilePath workingDirectory(Job context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException { // JENKINS-10880: workspace can be null if (workspace == null) { return null; @@ -1358,14 +1830,18 @@ protected FilePath workingDirectory(AbstractProject context, FilePath works * * @param git GitClient object * @param r Revision object - * @param listener + * @param listener build log * @return true if any exclusion files are matched, false otherwise. */ private boolean isRevExcluded(GitClient git, Revision r, TaskListener listener, BuildData buildData) throws IOException, InterruptedException { try { List revShow; if (buildData != null && buildData.lastBuild != null) { - revShow = git.showRevision(buildData.lastBuild.revision.getSha1(), r.getSha1()); + if (getExtensions().get(PathRestriction.class) != null) { + revShow = git.showRevision(buildData.lastBuild.revision.getSha1(), r.getSha1()); + } else { + revShow = git.showRevision(buildData.lastBuild.revision.getSha1(), r.getSha1(), false); + } } else { revShow = git.showRevision(r.getSha1()); } @@ -1375,7 +1851,8 @@ private boolean isRevExcluded(GitClient git, Revision r, TaskListener listener, int start=0, idx=0; for (String line : revShow) { if (line.startsWith("commit ") && idx!=0) { - GitChangeSet change = new GitChangeSet(revShow.subList(start,idx), getExtensions().get(AuthorInChangelog.class)!=null); + boolean showEntireCommitSummary = GitChangeSet.isShowEntireCommitSummaryInChanges() || !(git instanceof CliGitAPIImpl); + GitChangeSet change = new GitChangeSet(revShow.subList(start,idx), getExtensions().get(AuthorInChangelog.class)!=null, showEntireCommitSummary); Boolean excludeThisCommit=null; for (GitSCMExtension ext : extensions) { @@ -1401,10 +1878,10 @@ private boolean isRevExcluded(GitClient git, Revision r, TaskListener listener, } } - @Initializer(after=PLUGINS_STARTED) public static void onLoaded() { - DescriptorImpl desc = Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); + Jenkins jenkins = Jenkins.get(); + DescriptorImpl desc = jenkins.getDescriptorByType(DescriptorImpl.class); if (desc.getOldGitExe() != null) { String exe = desc.getOldGitExe(); @@ -1429,6 +1906,7 @@ public static void configureXtream() { * Set to true to enable more logging to build's {@link TaskListener}. * Used by various classes in this package. */ + @SuppressFBWarnings(value="MS_SHOULD_BE_FINAL", justification="Not final so users can adjust log verbosity") public static boolean VERBOSE = Boolean.getBoolean(GitSCM.class.getName() + ".verbose"); /** diff --git a/src/main/java/hudson/plugins/git/GitSCMBackwardCompatibility.java b/src/main/java/hudson/plugins/git/GitSCMBackwardCompatibility.java index dfb24a186a..408e9cb57d 100644 --- a/src/main/java/hudson/plugins/git/GitSCMBackwardCompatibility.java +++ b/src/main/java/hudson/plugins/git/GitSCMBackwardCompatibility.java @@ -17,6 +17,7 @@ import java.util.Set; import static org.apache.commons.lang.StringUtils.isNotBlank; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; /** * This is a portion of {@link GitSCM} for the stuff that's used to be in {@link GitSCM} @@ -178,6 +179,7 @@ public abstract class GitSCMBackwardCompatibility extends SCM implements Seriali private transient BuildChooser buildChooser; + @Whitelisted abstract DescribableList getExtensions(); @Override @@ -204,7 +206,7 @@ void readBackExtensionsFromLegacy() { skipTag = null; } if (disableSubmodules || recursiveSubmodules || trackingSubmodules) { - addIfMissing(new SubmoduleOption(disableSubmodules, recursiveSubmodules, trackingSubmodules)); + addIfMissing(new SubmoduleOption(disableSubmodules, recursiveSubmodules, trackingSubmodules, null, null, false)); } if (isNotBlank(gitConfigName) || isNotBlank(gitConfigEmail)) { addIfMissing(new UserIdentity(gitConfigName,gitConfigEmail)); @@ -364,6 +366,7 @@ public UserMergeOptions getUserMergeOptions() { /** * @deprecated * Moved to {@link CleanCheckout} + * @return true if clean before checkout extension is enabled */ public boolean getClean() { return getExtensions().get(CleanCheckout.class)!=null; @@ -372,6 +375,7 @@ public boolean getClean() { /** * @deprecated * Moved to {@link WipeWorkspace} + * @return true if wipe workspace extension is enabled */ public boolean getWipeOutWorkspace() { return getExtensions().get(WipeWorkspace.class)!=null; @@ -380,6 +384,7 @@ public boolean getWipeOutWorkspace() { /** * @deprecated * Moved to {@link CloneOption} + * @return true if shallow clone extension is enabled and shallow clone is configured */ public boolean getUseShallowClone() { CloneOption m = getExtensions().get(CloneOption.class); @@ -389,6 +394,7 @@ public boolean getUseShallowClone() { /** * @deprecated * Moved to {@link CloneOption} + * @return reference repository or null if reference repository is not defined */ public String getReference() { CloneOption m = getExtensions().get(CloneOption.class); @@ -398,6 +404,7 @@ public String getReference() { /** * @deprecated * Moved to {@link hudson.plugins.git.extensions.impl.DisableRemotePoll} + * @return true if remote polling is allowed */ public boolean getRemotePoll() { return getExtensions().get(DisableRemotePoll.class)==null; @@ -409,6 +416,7 @@ public boolean getRemotePoll() { * * @deprecated * Moved to {@link AuthorInChangelog} + * @return true if commit author is used as the changeset author */ public boolean getAuthorOrCommitter() { return getExtensions().get(AuthorInChangelog.class)!=null; @@ -417,6 +425,7 @@ public boolean getAuthorOrCommitter() { /** * @deprecated * Moved to {@link IgnoreNotifyCommit} + * @return true if commit notifications are ignored */ public boolean isIgnoreNotifyCommit() { return getExtensions().get(IgnoreNotifyCommit.class)!=null; @@ -425,6 +434,7 @@ public boolean isIgnoreNotifyCommit() { /** * @deprecated * Moved to {@link ScmName} + * @return configured SCM name or null if none if not configured */ public String getScmName() { ScmName sn = getExtensions().get(ScmName.class); @@ -434,6 +444,7 @@ public String getScmName() { /** * @deprecated * Moved to {@link LocalBranch} + * @return name of local branch used for checkout or null if LocalBranch extension is not enabled */ public String getLocalBranch() { LocalBranch lb = getExtensions().get(LocalBranch.class); diff --git a/src/main/java/hudson/plugins/git/GitStatus.java b/src/main/java/hudson/plugins/git/GitStatus.java index 177b5bff92..126530b558 100644 --- a/src/main/java/hudson/plugins/git/GitStatus.java +++ b/src/main/java/hudson/plugins/git/GitStatus.java @@ -1,78 +1,144 @@ package hudson.plugins.git; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.ExtensionPoint; import hudson.Util; -import hudson.model.AbstractModelObject; -import hudson.model.AbstractProject; -import hudson.model.Cause; -import hudson.model.Hudson; -import hudson.model.ParametersAction; -import hudson.model.UnprotectedRootAction; +import hudson.model.*; import hudson.plugins.git.extensions.impl.IgnoreNotifyCommit; import hudson.scm.SCM; import hudson.security.ACL; +import hudson.security.ACLContext; import hudson.triggers.SCMTrigger; -import jenkins.model.Jenkins; -import org.acegisecurity.context.SecurityContextHolder; -import org.apache.commons.lang.StringUtils; -import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.URIish; -import org.kohsuke.stapler.*; - -import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintWriter; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.logging.Level; import java.util.logging.Logger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_OK; +import jenkins.model.Jenkins; +import jenkins.scm.api.SCMEvent; +import jenkins.triggers.SCMTriggerItem; +import org.apache.commons.lang.StringUtils; import static org.apache.commons.lang.StringUtils.isNotEmpty; - -import org.acegisecurity.context.SecurityContext; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; +import org.kohsuke.stapler.*; /** * Information screen for the use of Git in Hudson. */ @Extension -public class GitStatus extends AbstractModelObject implements UnprotectedRootAction { +public class GitStatus implements UnprotectedRootAction { + @Override public String getDisplayName() { return "Git"; } - public String getSearchUrl() { - return getUrlName(); - } - public String getIconFileName() { // TODO return null; } + @Override public String getUrlName() { return "git"; } - public HttpResponse doNotifyCommit(@QueryParameter(required=true) String url, + /* Package protected - not part of API, needed for testing */ + /* package */ + static void setAllowNotifyCommitParameters(boolean allowed) { + allowNotifyCommitParameters = allowed; + } + + private String lastURL = ""; // Required query parameter + private String lastBranches = null; // Optional query parameter + private String lastSHA1 = null; // Optional query parameter + private List lastBuildParameters = null; + private static List lastStaticBuildParameters = null; + + private static void clearLastStaticBuildParameters() { + lastStaticBuildParameters = null; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + + s.append("URL: "); + s.append(lastURL); + + if (lastSHA1 != null) { + s.append(" SHA1: "); + s.append(lastSHA1); + } + + if (lastBranches != null) { + s.append(" Branches: "); + s.append(lastBranches); + } + + if (lastBuildParameters != null && !lastBuildParameters.isEmpty()) { + s.append(" Parameters: "); + for (ParameterValue buildParameter : lastBuildParameters) { + s.append(buildParameter.getName()); + s.append("='"); + s.append(buildParameter.getValue()); + s.append("',"); + } + s.delete(s.length() - 1, s.length()); + } + + if (lastStaticBuildParameters != null && !lastStaticBuildParameters.isEmpty()) { + s.append(" More parameters: "); + for (ParameterValue buildParameter : lastStaticBuildParameters) { + s.append(buildParameter.getName()); + s.append("='"); + s.append(buildParameter.getValue()); + s.append("',"); + } + s.delete(s.length() - 1, s.length()); + } + + return s.toString(); + } + + public HttpResponse doNotifyCommit(HttpServletRequest request, @QueryParameter(required=true) String url, @QueryParameter(required=false) String branches, @QueryParameter(required=false) String sha1) throws ServletException, IOException { + lastURL = url; + lastBranches = branches; + lastSHA1 = sha1; + lastBuildParameters = null; + GitStatus.clearLastStaticBuildParameters(); URIish uri; + List buildParameters = new ArrayList<>(); + try { uri = new URIish(url); } catch (URISyntaxException e) { return HttpResponses.error(SC_BAD_REQUEST, new Exception("Illegal URL: " + url, e)); } + if (allowNotifyCommitParameters || !safeParameters.isEmpty()) { // Allow SECURITY-275 bug + final Map parameterMap = request.getParameterMap(); + for (Map.Entry entry : parameterMap.entrySet()) { + if (!(entry.getKey().equals("url")) && !(entry.getKey().equals("branches")) && !(entry.getKey().equals("sha1"))) + if (entry.getValue()[0] != null && (allowNotifyCommitParameters || safeParameters.contains(entry.getKey()))) + buildParameters.add(new StringParameterValue(entry.getKey(), entry.getValue()[0])); + } + } + lastBuildParameters = buildParameters; + branches = Util.fixEmptyAndTrim(branches); + String[] branchesArray; if (branches == null) { branchesArray = new String[0]; @@ -80,46 +146,38 @@ public HttpResponse doNotifyCommit(@QueryParameter(required=true) String url, branchesArray = branches.split(","); } - final List contributors = new ArrayList(); - for (Listener listener : Jenkins.getInstance().getExtensionList(Listener.class)) { - contributors.addAll(listener.onNotifyCommit(uri, sha1, branchesArray)); + final List contributors = new ArrayList<>(); + Jenkins jenkins = Jenkins.get(); + String origin = SCMEvent.originOf(request); + for (Listener listener : jenkins.getExtensionList(Listener.class)) { + contributors.addAll(listener.onNotifyCommit(origin, uri, sha1, buildParameters, branchesArray)); } - return new HttpResponse() { - public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) - throws IOException, ServletException { - rsp.setStatus(SC_OK); - rsp.setContentType("text/plain"); - for (ResponseContributor c : contributors) { - c.addHeaders(req, rsp); - } - PrintWriter w = rsp.getWriter(); - for (ResponseContributor c : contributors) { - c.writeBody(req, rsp, w); + return (StaplerRequest req, StaplerResponse rsp, Object node) -> { + rsp.setStatus(SC_OK); + rsp.setContentType("text/plain"); + for (int i = 0; i < contributors.size(); i++) { + if (i == MAX_REPORTED_CONTRIBUTORS) { + rsp.addHeader("Triggered", "<" + (contributors.size() - i) + " more>"); + break; + } else { + contributors.get(i).addHeaders(req, rsp); } } - }; - } - - private static Collection getProjectScms(AbstractProject project) { - Set projectScms = Sets.newHashSet(); - if (Jenkins.getInstance().getPlugin("multiple-scms") != null) { - MultipleScmResolver multipleScmResolver = new MultipleScmResolver(); - multipleScmResolver.resolveMultiScmIfConfigured(project, projectScms); - } - if (projectScms.isEmpty()) { - SCM scm = project.getScm(); - if (scm instanceof GitSCM) { - projectScms.add(((GitSCM) scm)); + PrintWriter w = rsp.getWriter(); + for (ResponseContributor c : contributors) { + c.writeBody(req, rsp, w); } - } - return projectScms; + }; } /** * Used to test if what we have in the job configuration matches what was submitted to the notification endpoint. * It is better to match loosely and wastes a few polling calls than to be pedantic and miss the push notification, * especially given that Git tends to support multiple access protocols. + * @param lhs left-hand side of comparison + * @param rhs right-hand side of comparison + * @return true if left-hand side loosely matches right-hand side */ public static boolean looselyMatches(URIish lhs, URIish rhs) { return StringUtils.equals(lhs.getHost(),rhs.getHost()) @@ -134,7 +192,7 @@ private static String normalizePath(String path) { } /** - * Contributes to a {@link #doNotifyCommit(String, String, String)} response. + * Contributes to a {@link #doNotifyCommit(HttpServletRequest, String, String, String)} response. * * @since 1.4.1 */ @@ -179,25 +237,73 @@ public void writeBody(PrintWriter w) { public static abstract class Listener implements ExtensionPoint { /** - * Called when there is a change notification on a specific repository url. - * - * @param uri the repository uri. - * @param branches the (optional) branch information. + * @deprecated implement {@link #onNotifyCommit(org.eclipse.jgit.transport.URIish, String, List, String...)} + * @param uri the repository uri. + * @param branches the (optional) branch information. * @return any response contributors for the response to the push request. - * @since 1.4.1 - * @deprecated implement #onNotifyCommit(org.eclipse.jgit.transport.URIish, String, String...) */ public List onNotifyCommit(URIish uri, String[] branches) { - return onNotifyCommit(uri, null, branches); + throw new AbstractMethodError(); } + /** + * @deprecated implement {@link #onNotifyCommit(org.eclipse.jgit.transport.URIish, String, List, String...)} + * @param uri the repository uri. + * @param sha1 SHA1 hash of commit to build + * @param branches the (optional) branch information. + * @return any response contributors for the response to the push request. + */ public List onNotifyCommit(URIish uri, @Nullable String sha1, String... branches) { - return Collections.EMPTY_LIST; + return onNotifyCommit(uri, branches); } + + /** + * Called when there is a change notification on a specific repository url. + * + * @param uri the repository uri. + * @param sha1 SHA1 hash of commit to build + * @param buildParameters parameters to be passed to the build. + * Ignored unless build parameter flag is set + * due to security risk of accepting parameters from + * unauthenticated sources + * @param branches the (optional) branch information. + * @return any response contributors for the response to the push request. + * @since 2.4.0 + * @deprecated use {@link #onNotifyCommit(String, URIish, String, List, String...)} + */ + @Deprecated + public List onNotifyCommit(URIish uri, @Nullable String sha1, List buildParameters, String... branches) { + return onNotifyCommit(uri, sha1, branches); + } + + /** + * Called when there is a change notification on a specific repository url. + * + * @param origin the origin of the notification (use {@link SCMEvent#originOf(HttpServletRequest)} if in + * doubt) or {@code null} if the origin is unknown. + * @param uri the repository uri. + * @param sha1 SHA1 hash of commit to build + * @param buildParameters parameters to be passed to the build. + * Ignored unless build parameter flag is set + * due to security risk of accepting parameters from + * unauthenticated sources + * @param branches the (optional) branch information. + * @return any response contributors for the response to the push request. + * @since 2.6.5 + */ + public List onNotifyCommit(@CheckForNull String origin, + URIish uri, + @Nullable String sha1, + List buildParameters, + String... branches) { + return onNotifyCommit(uri, sha1, buildParameters, branches); + } + + } /** - * Handle standard {@link AbstractProject} instances with a standard {@link SCMTrigger}. + * Handle standard {@link SCMTriggerItem} instances with a standard {@link SCMTrigger}. * * @since 1.4.1 */ @@ -209,28 +315,46 @@ public static class JenkinsAbstractProjectListener extends Listener { * {@inheritDoc} */ @Override - public List onNotifyCommit(URIish uri, String sha1, String... branches) { - List result = new ArrayList(); + public List onNotifyCommit(String origin, URIish uri, String sha1, List buildParameters, String... branches) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Received notification from {0} for uri = {1} ; sha1 = {2} ; branches = {3}", + new Object[]{StringUtils.defaultIfBlank(origin, "?"), uri, sha1, Arrays.toString(branches)}); + } + + GitStatus.clearLastStaticBuildParameters(); + List allBuildParameters = new ArrayList<>(buildParameters); + List result = new ArrayList<>(); // run in high privilege to see all the projects anonymous users don't see. // this is safe because when we actually schedule a build, it's a build that can // happen at some random time anyway. - SecurityContext old = ACL.impersonate(ACL.SYSTEM); - try { - - final List> projects = Lists.newArrayList(); + try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { boolean scmFound = false, urlFound = false; - for (final AbstractProject project : Hudson.getInstance().getAllItems(AbstractProject.class)) { - Collection projectSCMs = getProjectScms(project); - for (GitSCM git : projectSCMs) { + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + LOGGER.severe("Jenkins.getInstance() is null in GitStatus.onNotifyCommit"); + return result; + } + for (final Item project : jenkins.getAllItems()) { + SCMTriggerItem scmTriggerItem = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(project); + if (scmTriggerItem == null) { + continue; + } + SCMS: for (SCM scm : scmTriggerItem.getSCMs()) { + if (!(scm instanceof GitSCM)) { + continue; + } + GitSCM git = (GitSCM) scm; scmFound = true; for (RemoteConfig repository : git.getRepositories()) { boolean repositoryMatches = false, branchMatches = false; + URIish matchedURL = null; for (URIish remoteURL : repository.getURIs()) { if (looselyMatches(uri, remoteURL)) { repositoryMatches = true; + matchedURL = remoteURL; break; } } @@ -239,39 +363,78 @@ public List onNotifyCommit(URIish uri, String sha1, String. continue; } - SCMTrigger trigger = project.getTrigger(SCMTrigger.class); - if (trigger != null && trigger.isIgnorePostCommitHooks()) { - LOGGER.info("PostCommitHooks are disabled on " + project.getFullDisplayName()); + SCMTrigger trigger = scmTriggerItem.getSCMTrigger(); + if (trigger == null || trigger.isIgnorePostCommitHooks()) { + LOGGER.log(Level.INFO, "no trigger, or post-commit hooks disabled, on {0}", project.getFullDisplayName()); continue; } - Boolean branchFound = false; + boolean branchFound = false, + parametrizedBranchSpec = false; if (branches.length == 0) { branchFound = true; } else { OUT: for (BranchSpec branchSpec : git.getBranches()) { - for (String branch : branches) { - if (branchSpec.matches(repository.getName() + "/" + branch)) { - branchFound = true; - break OUT; + if (branchSpec.getName().contains("$")) { + // If the branchspec is parametrized, always run the polling + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Branch Spec is parametrized for {0}", project.getFullDisplayName()); + } + branchFound = true; + parametrizedBranchSpec = true; + } else { + for (String branch : branches) { + if (branchSpec.matchesRepositoryBranch(repository.getName(), branch)) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Branch Spec {0} matches modified branch {1} for {2}", new Object[]{branchSpec, branch, project.getFullDisplayName()}); + } + branchFound = true; + break OUT; + } } } } } if (!branchFound) continue; urlFound = true; + if (!(project instanceof AbstractProject && ((AbstractProject) project).isDisabled())) { + //JENKINS-30178 Add default parameters defined in the job + if (project instanceof Job) { + Set buildParametersNames = new HashSet<>(); + if (allowNotifyCommitParameters || !safeParameters.isEmpty()) { + for (ParameterValue parameterValue: allBuildParameters) { + if (allowNotifyCommitParameters || safeParameters.contains(parameterValue.getName())) { + buildParametersNames.add(parameterValue.getName()); + } + } + } - if (!project.isDisabled()) { - if (isNotEmpty(sha1)) { - LOGGER.info("Scheduling " + project.getFullDisplayName() + " to build commit " + sha1); - project.scheduleBuild2(project.getQuietPeriod(), - new CommitHookCause(sha1), - new RevisionParameterAction(sha1)); + List jobParametersValues = getDefaultParametersValues((Job) project); + for (ParameterValue defaultParameterValue : jobParametersValues) { + if (!buildParametersNames.contains(defaultParameterValue.getName())) { + allBuildParameters.add(defaultParameterValue); + } + } + } + if (!parametrizedBranchSpec && isNotEmpty(sha1)) { + /* If SHA1 and not a parameterized branch spec, then schedule build. + * NOTE: This is SCHEDULING THE BUILD, not triggering polling of the repo. + * If no SHA1 or the branch spec is parameterized, it will only poll. + */ + LOGGER.log(Level.INFO, "Scheduling {0} to build commit {1}", new Object[]{project.getFullDisplayName(), sha1}); + scmTriggerItem.scheduleBuild2(scmTriggerItem.getQuietPeriod(), + new CauseAction(new CommitHookCause(sha1)), + new RevisionParameterAction(sha1, matchedURL), new ParametersAction(allBuildParameters)); result.add(new ScheduledResponseContributor(project)); - } else if (trigger != null) { - LOGGER.info("Triggering the polling of " + project.getFullDisplayName()); + } else { + /* Poll the repository for changes + * NOTE: This is not scheduling the build, just polling for changes + * If the polling detects changes, it will schedule the build + */ + LOGGER.log(Level.INFO, "Triggering the polling of {0}", project.getFullDisplayName()); trigger.run(); result.add(new PollingScheduledResponseContributor(project)); + break SCMS; // no need to trigger the same project twice, so do not consider other GitSCMs in it } } break; @@ -287,14 +450,42 @@ public List onNotifyCommit(URIish uri, String sha1, String. .join(branches, ","))); } + lastStaticBuildParameters = allBuildParameters; return result; - } finally { - SecurityContextHolder.setContext(old); } } /** - * A response contributor for triggering polling of an {@link AbstractProject}. + * Get the default parameters values from a job + * + */ + private ArrayList getDefaultParametersValues(Job job) { + ArrayList defValues; + ParametersDefinitionProperty paramDefProp = job.getProperty(ParametersDefinitionProperty.class); + + if (paramDefProp != null) { + List parameterDefinition = paramDefProp.getParameterDefinitions(); + defValues = new ArrayList<>(parameterDefinition.size()); + + } else { + defValues = new ArrayList<>(); + return defValues; + } + + /* Scan for all parameter with an associated default values */ + for (ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) { + ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); + + if (defaultValue != null) { + defValues.add(defaultValue); + } + } + + return defValues; + } + + /** + * A response contributor for triggering polling of a project. * * @since 1.4.1 */ @@ -302,14 +493,14 @@ private static class PollingScheduledResponseContributor extends ResponseContrib /** * The project */ - private final AbstractProject project; + private final Item project; /** * Constructor. * * @param project the project. */ - public PollingScheduledResponseContributor(AbstractProject project) { + public PollingScheduledResponseContributor(Item project) { this.project = project; } @@ -334,14 +525,14 @@ private static class ScheduledResponseContributor extends ResponseContributor { /** * The project */ - private final AbstractProject project; + private final Item project; /** * Constructor. * * @param project the project. */ - public ScheduledResponseContributor(AbstractProject project) { + public ScheduledResponseContributor(Item project) { this.project = project; } @@ -407,5 +598,71 @@ public String getShortDescription() { } private static final Logger LOGGER = Logger.getLogger(GitStatus.class.getName()); -} + private static final int MAX_REPORTED_CONTRIBUTORS = 10; + /** Allow arbitrary notify commit parameters. + * + * SECURITY-275 detected that allowing arbitrary parameters through + * the notifyCommit URL allows an unauthenticated user to set + * environment variables for a job. + * + * If this property is set to true, then the bug exposed by + * SECURITY-275 will be brought back. Only enable this if you + * trust all unauthenticated users to not pass harmful arguments + * to your jobs. + * + * -Dhudson.plugins.git.GitStatus.allowNotifyCommitParameters=true on command line + * + * Also honors the global Jenkins security setting + * "hudson.model.ParametersAction.keepUndefinedParameters" if it + * is set to true. + */ + public static final boolean ALLOW_NOTIFY_COMMIT_PARAMETERS = Boolean.valueOf(System.getProperty(GitStatus.class.getName() + ".allowNotifyCommitParameters", "false")) + || Boolean.valueOf(System.getProperty("hudson.model.ParametersAction.keepUndefinedParameters", "false")); + private static boolean allowNotifyCommitParameters = ALLOW_NOTIFY_COMMIT_PARAMETERS; + + /* Package protected for test. + * If null is passed as argument, safe parameters are reset to defaults. + */ + static void setSafeParametersForTest(String parameters) { + safeParameters = csvToSet(parameters != null ? parameters : SAFE_PARAMETERS); + } + + private static Set csvToSet(String csvLine) { + String[] tokens = csvLine.split(","); + Set set = new HashSet<>(Arrays.asList(tokens)); + return set; + } + + @NonNull + private static String getSafeParameters() { + String globalSafeParameters = System.getProperty("hudson.model.ParametersAction.safeParameters", "").trim(); + String gitStatusSafeParameters = System.getProperty(GitStatus.class.getName() + ".safeParameters", "").trim(); + if (globalSafeParameters.isEmpty()) { + return gitStatusSafeParameters; + } + if (gitStatusSafeParameters.isEmpty()) { + return globalSafeParameters; + } + return globalSafeParameters + "," + gitStatusSafeParameters; + } + + /** + * Allow specifically declared safe parameters. + * + * SECURITY-275 detected that allowing arbitrary parameters through the + * notifyCommit URL allows an unauthenticated user to set environment + * variables for a job. + * + * If this property is set to a comma separated list of parameters, then + * those parameters will be allowed for any job. Only set this value for + * parameters you trust in all the jobs in your system. + * + * -Dhudson.plugins.git.GitStatus.safeParameters=PARM1,PARM1 on command line + * + * Also honors the global Jenkins safe parameter list + * "hudson.model.ParametersAction.safeParameters" if set. + */ + public static final String SAFE_PARAMETERS = getSafeParameters(); + private static Set safeParameters = csvToSet(SAFE_PARAMETERS); +} diff --git a/src/main/java/hudson/plugins/git/GitStatusCrumbExclusion.java b/src/main/java/hudson/plugins/git/GitStatusCrumbExclusion.java new file mode 100644 index 0000000000..72ac266b44 --- /dev/null +++ b/src/main/java/hudson/plugins/git/GitStatusCrumbExclusion.java @@ -0,0 +1,32 @@ +package hudson.plugins.git; + +import hudson.Extension; +import hudson.security.csrf.CrumbExclusion; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Make POST to /git/notifyCommit work with CSRF protection on. + */ +@Extension +public class GitStatusCrumbExclusion extends CrumbExclusion { + + @Override + public boolean process(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) + throws IOException, ServletException { + String pathInfo = req.getPathInfo(); + if (pathInfo != null && pathInfo.equals(getExclusionPath())) { + chain.doFilter(req, resp); + return true; + } + return false; + } + + public String getExclusionPath() { + return "/git/notifyCommit"; + } +} diff --git a/src/main/java/hudson/plugins/git/GitTagAction.java b/src/main/java/hudson/plugins/git/GitTagAction.java index f2a0eb7bf3..adc2fdcff0 100644 --- a/src/main/java/hudson/plugins/git/GitTagAction.java +++ b/src/main/java/hudson/plugins/git/GitTagAction.java @@ -4,8 +4,6 @@ import hudson.Extension; import hudson.FilePath; import hudson.model.*; -import hudson.plugins.git.util.BuildData; -import hudson.remoting.VirtualChannel; import hudson.scm.AbstractScmTagAction; import hudson.security.Permission; import hudson.util.CopyOnWriteMap; @@ -17,6 +15,7 @@ import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.stapler.interceptor.RequirePOST; import javax.servlet.ServletException; import java.io.File; @@ -24,7 +23,7 @@ import java.util.*; /** - * @author Vivek Pandey + * @author Nicolas de Loof */ @ExportedBean public class GitTagAction extends AbstractScmTagAction implements Describable { @@ -34,21 +33,24 @@ public class GitTagAction extends AbstractScmTagAction implements Describable> tags = new CopyOnWriteMap.Tree>(); + private final Map> tags = new CopyOnWriteMap.Tree<>(); private final String ws; - protected GitTagAction(AbstractBuild build, BuildData buildData) { + private String lastTagName = null; + private GitException lastTagException = null; + + protected GitTagAction(Run build, FilePath workspace, Revision revision) { super(build); - List val = new ArrayList(); - this.ws = build.getWorkspace().getRemote(); - for (Branch b : buildData.lastBuild.revision.getBranches()) { - tags.put(b.getName(), new ArrayList()); + this.ws = workspace.getRemote(); + for (Branch b : revision.getBranches()) { + tags.put(b.getName(), new ArrayList<>()); } } public Descriptor getDescriptor() { - return Jenkins.getInstance().getDescriptorOrDie(getClass()); + Jenkins jenkins = Jenkins.get(); + return jenkins.getDescriptorOrDie(getClass()); } @Override @@ -59,12 +61,14 @@ public boolean isTagged() { return false; } + @Override public String getIconFileName() { if (!isTagged() && !getACL().hasPermission(getPermission())) return null; return "save.gif"; } + @Override public String getDisplayName() { int nonNullTag = 0; for (List v : tags.values()) { @@ -77,13 +81,14 @@ public String getDisplayName() { if (nonNullTag == 0) return "No Tags"; if (nonNullTag == 1) - return "There is one tag"; + return "One tag"; else - return "There are more than one tag"; + return "Multiple tags"; } /** * @see #tags + * @return tag names and annotations for this repository */ public Map> getTags() { return Collections.unmodifiableMap(tags); @@ -91,7 +96,7 @@ public Map> getTags() { @Exported(name = "tags") public List getTagInfo() { - List data = new ArrayList(); + List data = new ArrayList<>(); for (Map.Entry> e : tags.entrySet()) { String module = e.getKey(); for (String tag : e.getValue()) @@ -102,7 +107,8 @@ public List getTagInfo() { @ExportedBean public static class TagInfo { - private String module, url; + private final String module; + private final String url; private TagInfo(String branch, String tag) { this.module = branch; @@ -135,25 +141,43 @@ public String getTooltip() { /** * Invoked to actually tag the workspace. + * @param req request for submit + * @param rsp response used to send result + * @throws IOException on input or output error + * @throws ServletException on servlet error */ + @RequirePOST public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { getACL().checkPermission(getPermission()); - MultipartFormDataParser parser = new MultipartFormDataParser(req); + try (MultipartFormDataParser parser = new MultipartFormDataParser(req)) { - Map newTags = new HashMap(); + Map newTags = new HashMap<>(); - int i = -1; - for (String e : tags.keySet()) { - i++; - if (tags.size() > 1 && parser.get("tag" + i) == null) - continue; // when tags.size()==1, UI won't show the checkbox. - newTags.put(e, parser.get("name" + i)); - } + int i = -1; + for (String e : tags.keySet()) { + i++; + if (tags.size() > 1 && parser.get("tag" + i) == null) + continue; // when tags.size()==1, UI won't show the checkbox. + newTags.put(e, parser.get("name" + i)); + } - new TagWorkerThread(newTags, parser.get("comment")).start(); + scheduleTagCreation(newTags, parser.get("comment")); - rsp.sendRedirect("."); + rsp.sendRedirect("."); + } + } + + /** + * Schedule creation of a tag. For test purposes only, not to be called outside this package. + * + * @param newTags tags to be created + * @param comment tag comment to be included with created tags + * @throws IOException on IO error + * @throws ServletException on servlet exception + */ + void scheduleTagCreation(Map newTags, String comment) throws IOException, ServletException { + new TagWorkerThread(newTags, comment).start(); } /** @@ -167,35 +191,37 @@ public final class TagWorkerThread extends TaskThread { private final String comment; public TagWorkerThread(Map tagSet,String comment) { - super(GitTagAction.this, ListenerAndText.forMemory()); + super(GitTagAction.this, ListenerAndText.forMemory(null)); this.tagSet = tagSet; this.comment = comment; } @Override protected void perform(final TaskListener listener) throws Exception { - final EnvVars environment = build.getEnvironment(listener); + final EnvVars environment = getRun().getEnvironment(listener); final FilePath workspace = new FilePath(new File(ws)); final GitClient git = Git.with(listener, environment) .in(workspace) .getClient(); - for (String b : tagSet.keySet()) { + for (Map.Entry entry : tagSet.entrySet()) { try { String buildNum = "jenkins-" - + build.getProject().getName().replace(" ", "_") - + "-" + tagSet.get(b); - git.tag(tagSet.get(b), "Jenkins Build #" + buildNum); + + getRun().getParent().getName().replace(" ", "_") + + "-" + entry.getValue(); + git.tag(entry.getValue(), "Jenkins Build #" + buildNum); + lastTagName = entry.getValue(); for (Map.Entry e : tagSet.entrySet()) GitTagAction.this.tags.get(e.getKey()).add(e.getValue()); - getBuild().save(); + getRun().save(); workerThread = null; } catch (GitException ex) { - ex.printStackTrace(listener.error("Error taggin repo '%s' : %s", b, ex.getMessage())); + lastTagException = ex; + ex.printStackTrace(listener.error("Error tagging repo '%s' : %s", entry.getKey(), ex.getMessage())); // Failed. Try the next one listener.getLogger().println("Trying next branch"); } @@ -214,8 +240,19 @@ public Permission getPermission() { */ @Extension public static class DescriptorImpl extends Descriptor { + @Override public String getDisplayName() { return "Tag"; } } + + /* Package protected for use only by tests */ + String getLastTagName() { + return lastTagName; + } + + /* Package protected for use only by tests */ + GitException getLastTagException() { + return lastTagException; + } } diff --git a/src/main/java/hudson/plugins/git/MultipleScmResolver.java b/src/main/java/hudson/plugins/git/MultipleScmResolver.java deleted file mode 100644 index 401d6405ae..0000000000 --- a/src/main/java/hudson/plugins/git/MultipleScmResolver.java +++ /dev/null @@ -1,27 +0,0 @@ -package hudson.plugins.git; - -import hudson.model.AbstractProject; -import hudson.scm.SCM; -import org.jenkinsci.plugins.multiplescms.MultiSCM; - -import java.util.List; -import java.util.Set; - -/** - * @author Noam Y. Tenne - */ -public class MultipleScmResolver { - - public void resolveMultiScmIfConfigured(AbstractProject project, Set projectScms) { - SCM projectScm = project.getScm(); - if (projectScm instanceof MultiSCM) { - List configuredSCMs = ((MultiSCM) projectScm).getConfiguredSCMs(); - for (SCM configuredSCM : configuredSCMs) { - if (configuredSCM instanceof GitSCM) { - projectScms.add(((GitSCM) configuredSCM)); - } - } - - } - } -} diff --git a/src/main/java/hudson/plugins/git/ObjectIdConverter.java b/src/main/java/hudson/plugins/git/ObjectIdConverter.java index 21fb862b18..d6ec3ffb05 100644 --- a/src/main/java/hudson/plugins/git/ObjectIdConverter.java +++ b/src/main/java/hudson/plugins/git/ObjectIdConverter.java @@ -37,8 +37,8 @@ public void marshal(Object source, HierarchicalStreamWriter writer, /** * Is the current reader node a legacy node? * - * @param reader - * @param context + * @param reader stream reader + * @param context usage context of reader * @return true if legacy, false otherwise */ protected boolean isLegacyNode(HierarchicalStreamReader reader, @@ -50,8 +50,8 @@ protected boolean isLegacyNode(HierarchicalStreamReader reader, /** * Legacy unmarshalling of object id * - * @param reader - * @param context + * @param reader stream reader + * @param context usage context of reader * @return object id */ protected Object legacyUnmarshal(HierarchicalStreamReader reader, diff --git a/src/main/java/hudson/plugins/git/RemoteConfigConverter.java b/src/main/java/hudson/plugins/git/RemoteConfigConverter.java index 9c11bacd9e..e1446f9af1 100644 --- a/src/main/java/hudson/plugins/git/RemoteConfigConverter.java +++ b/src/main/java/hudson/plugins/git/RemoteConfigConverter.java @@ -90,21 +90,32 @@ private void fromMap(Map> map) { for (Entry> entry : map.entrySet()) { String key = entry.getKey(); Collection values = entry.getValue(); - if (KEY_URL.equals(key)) - uris = values.toArray(new String[values.size()]); - else if (KEY_FETCH.equals(key)) - fetch = values.toArray(new String[values.size()]); - else if (KEY_PUSH.equals(key)) - push = values.toArray(new String[values.size()]); - else if (KEY_UPLOADPACK.equals(key)) - for (String value : values) - uploadpack = value; - else if (KEY_RECEIVEPACK.equals(key)) - for (String value : values) - receivepack = value; - else if (KEY_TAGOPT.equals(key)) - for (String value : values) - tagopt = value; + if (null != key) + switch (key) { + case KEY_URL: + uris = values.toArray(new String[0]); + break; + case KEY_FETCH: + fetch = values.toArray(new String[0]); + break; + case KEY_PUSH: + push = values.toArray(new String[0]); + break; + case KEY_UPLOADPACK: + for (String value : values) + uploadpack = value; + break; + case KEY_RECEIVEPACK: + for (String value : values) + receivepack = value; + break; + case KEY_TAGOPT: + for (String value : values) + tagopt = value; + break; + default: + break; + } } } @@ -112,13 +123,13 @@ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = in.readUTF(); final int items = in.readInt(); - Map> map = new HashMap>(); + Map> map = new HashMap<>(); for (int i = 0; i < items; i++) { String key = in.readUTF(); String value = in.readUTF(); Collection values = map.get(key); if (values == null) { - values = new ArrayList(); + values = new ArrayList<>(); map.put(key, values); } values.add(value); @@ -132,7 +143,7 @@ public void writeExternal(ObjectOutput out) throws IOException { /** * @return remote config - * @throws URISyntaxException + * @throws URISyntaxException on incorrect URI syntax */ public RemoteConfig toRemote() throws URISyntaxException { return new RemoteConfig(this, name); @@ -143,9 +154,9 @@ public RemoteConfig toRemote() throws URISyntaxException { private final SerializableConverter converter; /** - * Create remote config converter + * Create remote config converter. * - * @param xStream + * @param xStream XStream used for remote configuration conversion */ public RemoteConfigConverter(XStream xStream) { mapper = xStream.getMapper(); @@ -165,8 +176,8 @@ public void marshal(Object source, HierarchicalStreamWriter writer, /** * Is the current reader node a legacy node? * - * @param reader - * @param context + * @param reader stream reader + * @param context usage context of reader * @return true if legacy, false otherwise */ protected boolean isLegacyNode(HierarchicalStreamReader reader, @@ -177,8 +188,8 @@ protected boolean isLegacyNode(HierarchicalStreamReader reader, /** * Legacy unmarshalling of remote config * - * @param reader - * @param context + * @param reader stream reader + * @param context usage context of reader * @return remote config */ protected Object legacyUnmarshal(final HierarchicalStreamReader reader, @@ -218,11 +229,7 @@ public void close() { proxy.readExternal(objectInput); objectInput.popCallback(); return proxy.toRemote(); - } catch (IOException e) { - throw new ConversionException("Unmarshal failed", e); - } catch (ClassNotFoundException e) { - throw new ConversionException("Unmarshal failed", e); - } catch (URISyntaxException e) { + } catch (IOException | ClassNotFoundException | URISyntaxException e) { throw new ConversionException("Unmarshal failed", e); } } diff --git a/src/main/java/hudson/plugins/git/RevisionParameterAction.java b/src/main/java/hudson/plugins/git/RevisionParameterAction.java index 608544902b..881b70253e 100644 --- a/src/main/java/hudson/plugins/git/RevisionParameterAction.java +++ b/src/main/java/hudson/plugins/git/RevisionParameterAction.java @@ -29,10 +29,14 @@ import hudson.model.Queue; import hudson.model.Queue.QueueAction; import hudson.model.queue.FoldableAction; + import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import org.jenkinsci.plugins.gitclient.GitClient; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; @@ -50,15 +54,25 @@ public class RevisionParameterAction extends InvisibleAction implements Serializ public final String commit; public final boolean combineCommits; public final Revision revision; + private final URIish repoURL; public RevisionParameterAction(String commit) { - this(commit, false); + this(commit, false, null); + } + + public RevisionParameterAction(String commit, URIish repoURL) { + this(commit, false, repoURL); } public RevisionParameterAction(String commit, boolean combineCommits) { + this(commit, combineCommits, null); + } + + public RevisionParameterAction(String commit, boolean combineCommits, URIish repoURL) { this.commit = commit; this.combineCommits = combineCommits; this.revision = null; + this.repoURL = repoURL; } public RevisionParameterAction(Revision revision) { @@ -69,6 +83,7 @@ public RevisionParameterAction(Revision revision, boolean combineCommits) { this.revision = revision; this.commit = revision.getSha1String(); this.combineCommits = combineCommits; + this.repoURL = null; } @Deprecated @@ -82,11 +97,62 @@ public Revision toRevision(GitClient git) throws InterruptedException { } ObjectId sha1 = git.revParse(commit); Revision revision = new Revision(sha1); - // all we have is a sha1 so make the branch 'detached' - revision.getBranches().add(new Branch("detached", sha1)); + // Here we do not have any local branches, containing the commit. So... + // we are to get all the remote branches, and show them to users, as + // they are local + final List branches = normalizeBranches(git.getBranchesContaining( + ObjectId.toString(sha1), true)); + revision.getBranches().addAll(branches); return revision; } + /** + * This method tries to determine whether the commit is from given remotes. + * To achieve that it uses remote URL supplied during construction of this instance. + * + * @param remotes candidate remotes for this commit + * @return false if remote URL was supplied during construction and matches none + * of given remote URLs, otherwise true + */ + public boolean canOriginateFrom(Iterable remotes) { + if (repoURL == null) { + return true; + } + + for (RemoteConfig remote : remotes) { + for (URIish remoteURL : remote.getURIs()) { + if (remoteURL.equals(repoURL)) { + return true; + } + } + } + return false; + } + + /** + * This method is aimed to normalize all the branches to the same naming + * convention, as {@link GitClient#getBranchesContaining(String, boolean)} + * returns branches with "remotes/" prefix. + * @param branches branches, retrieved from git client + * @return list of branches without the "remote/" prefix. + */ + private List normalizeBranches(List branches) { + final List normalBranches = new ArrayList<>(branches.size()); + final String remotesPrefix = "remotes/"; + for (Branch initialBranch : branches) { + final String initialBranchName = initialBranch.getName(); + final Branch normalBranch; + if (initialBranchName.startsWith(remotesPrefix)) { + final String normalName = initialBranchName.substring(remotesPrefix.length()); + normalBranch = new Branch(normalName, initialBranch.getSHA1()); + } else { + normalBranch = initialBranch; + } + normalBranches.add(normalBranch); + } + return normalBranches; + } + @Override public String toString() { return super.toString()+"[commit="+commit+"]"; @@ -128,19 +194,13 @@ public boolean shouldSchedule(List actions) { public void foldIntoExisting(Queue.Item item, Queue.Task owner, List otherActions) { // only do this if we are asked to. if(combineCommits) { - RevisionParameterAction existing = item.getAction(RevisionParameterAction.class); - if (existing!=null) { - //because we cannot modify the commit in the existing action remove it and add self - item.getActions().remove(existing); - item.getActions().add(this); - return; - } - // no CauseAction found, so add a copy of this one - item.getActions().add(this); + //because we cannot modify the commit in the existing action remove it and add self + // or no CauseAction found, so add a copy of this one + item.replaceAction(this); } } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; private static final Logger LOGGER = Logger.getLogger(RevisionParameterAction.class.getName()); } diff --git a/src/main/java/hudson/plugins/git/SubmoduleCombinator.java b/src/main/java/hudson/plugins/git/SubmoduleCombinator.java index a0d1347a10..83f22237ec 100644 --- a/src/main/java/hudson/plugins/git/SubmoduleCombinator.java +++ b/src/main/java/hudson/plugins/git/SubmoduleCombinator.java @@ -1,6 +1,5 @@ package hudson.plugins.git; -import hudson.FilePath; import hudson.model.TaskListener; import hudson.plugins.git.util.GitUtils; import org.eclipse.jgit.lib.ObjectId; @@ -11,7 +10,7 @@ import java.util.Map.Entry; /** - * A common usecase for git submodules is to have child submodules, and a parent 'configuration' project that ties the + * A common use case for git submodules is to have child submodules, and a parent 'configuration' project that ties the * correct versions together. It is useful to be able to speculatively compile all combinations of submodules, so that * you can _know_ if a particular combination is no longer compatible. * @@ -19,7 +18,6 @@ */ public class SubmoduleCombinator { GitClient git; - FilePath workspace; TaskListener listener; long tid = new Date().getTime(); @@ -30,13 +28,11 @@ public class SubmoduleCombinator { public SubmoduleCombinator(GitClient git, TaskListener listener, Collection cfg) { this.git = git; this.listener = listener; - - this.workspace = git.getWorkTree(); this.submoduleConfig = cfg; } public void createSubmoduleCombinations() throws GitException, IOException, InterruptedException { - Map> moduleBranches = new HashMap>(); + Map> moduleBranches = new HashMap<>(); for (IndexEntry submodule : git.getSubmodules("HEAD")) { GitClient subGit = git.subGit(submodule.getFile()); @@ -68,7 +64,7 @@ public void createSubmoduleCombinations() throws GitException, IOException, Inte listener.getLogger().println("There are " + combinations.size() + " submodule/revision combinations possible"); // Create a map which is SHA1 -> Submodule IDs that were present - Map> entriesMap = new HashMap>(); + Map> entriesMap = new HashMap<>(); // Knock out already-defined configurations for (ObjectId sha1 : git.revListAll()) { // What's the submodule configuration @@ -112,7 +108,7 @@ public void createSubmoduleCombinations() throws GitException, IOException, Inte if (min == 1) break; // look no further } - git.checkout(sha1.name()); + git.checkout().ref(sha1.name()).execute(); makeCombination(combination); } @@ -173,13 +169,14 @@ public int difference(Map item, List entries) for (IndexEntry entry : entries) { Revision b = null; - for (IndexEntry e : item.keySet()) { - if (e.getFile().equals(entry.getFile())) b = item.get(e); + for (Map.Entry entryAndRevision : item.entrySet()) { + IndexEntry e = entryAndRevision.getKey(); + if (e.getFile().equals(entry.getFile())) b = entryAndRevision.getValue(); } if (b == null) return -1; - if (!entry.getObject().equals(b.getSha1())) difference++; + if (!entry.getObject().equals(b.getSha1().getName())) difference++; } return difference; @@ -191,30 +188,30 @@ protected boolean matches(Map item, List entri public List> createCombinations(Map> moduleBranches) { - if (moduleBranches.keySet().size() == 0) return new ArrayList>(); + if (moduleBranches.keySet().isEmpty()) return new ArrayList<>(); // Get an entry: - List> thisLevel = new ArrayList>(); + List> thisLevel = new ArrayList<>(); IndexEntry e = moduleBranches.keySet().iterator().next(); for (Revision b : moduleBranches.remove(e)) { - Map result = new HashMap(); + Map result = new HashMap<>(); result.put(e, b); thisLevel.add(result); } List> children = createCombinations(moduleBranches); - if (children.size() == 0) return thisLevel; + if (children.isEmpty()) return thisLevel; // Merge the two together - List> result = new ArrayList>(); + List> result = new ArrayList<>(); for (Map thisLevelEntry : thisLevel) { for (Map childLevelEntry : children) { - HashMap r = new HashMap(); + HashMap r = new HashMap<>(); r.putAll(thisLevelEntry); r.putAll(childLevelEntry); result.add(r); diff --git a/src/main/java/hudson/plugins/git/SubmoduleConfig.java b/src/main/java/hudson/plugins/git/SubmoduleConfig.java index bc6ff2b5ba..fa0b023f32 100644 --- a/src/main/java/hudson/plugins/git/SubmoduleConfig.java +++ b/src/main/java/hudson/plugins/git/SubmoduleConfig.java @@ -1,7 +1,14 @@ package hudson.plugins.git; import com.google.common.base.Joiner; +import org.apache.commons.collections.CollectionUtils; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.kohsuke.stapler.DataBoundConstructor; +import java.util.Arrays; + +import java.util.Collection; +import java.util.Collections; import java.util.regex.Pattern; public class SubmoduleConfig implements java.io.Serializable { @@ -9,6 +16,23 @@ public class SubmoduleConfig implements java.io.Serializable { String submoduleName; String[] branches; + public SubmoduleConfig() { + this(null, Collections.emptySet()); + } + + public SubmoduleConfig(String submoduleName, String[] branches) { + this(submoduleName, branches != null ? Arrays.asList(branches) : Collections.emptySet()); + } + + @DataBoundConstructor + public SubmoduleConfig(String submoduleName, Collection branches) { + this.submoduleName = submoduleName; + if (CollectionUtils.isNotEmpty(branches)) { + this.branches = branches.toArray(new String[0]); + } + } + + @Whitelisted public String getSubmoduleName() { return submoduleName; } @@ -18,11 +42,21 @@ public void setSubmoduleName(String submoduleName) { } public String[] getBranches() { - return branches; + /* findbugs correctly complains that returning branches exposes the + * internal representation of the class to callers. Returning a copy + * of the array does not expose internal representation, at the possible + * expense of some additional memory. + */ + return Arrays.copyOf(branches, branches.length); } public void setBranches(String[] branches) { - this.branches = branches; + /* findbugs correctly complains that assign to branches exposes the + * internal representation of the class to callers. Assigning a copy + * of the array does not expose internal representation, at the possible + * expense of some additional memory. + */ + this.branches = Arrays.copyOf(branches, branches.length); } public boolean revisionMatchesInterest(Revision r) { diff --git a/src/main/java/hudson/plugins/git/UserMergeOptions.java b/src/main/java/hudson/plugins/git/UserMergeOptions.java index 249e7c0a9d..e4ff9915a2 100644 --- a/src/main/java/hudson/plugins/git/UserMergeOptions.java +++ b/src/main/java/hudson/plugins/git/UserMergeOptions.java @@ -1,15 +1,21 @@ package hudson.plugins.git; import hudson.Extension; +import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.plugins.git.opt.PreBuildMergeOptions; -import hudson.util.ListBoxModel; import org.jenkinsci.plugins.gitclient.MergeCommand; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.export.Exported; import java.io.Serializable; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.jenkinsci.plugins.structs.describable.CustomDescribableModel; +import org.kohsuke.stapler.DataBoundSetter; /** * User-provided configuration that dictates which branch in which repository we'll be @@ -19,35 +25,77 @@ public class UserMergeOptions extends AbstractDescribableImpl implements Serializable { private String mergeRemote; - private String mergeTarget; + private final String mergeTarget; private String mergeStrategy; + private MergeCommand.GitPluginFastForwardMode fastForwardMode; - @DataBoundConstructor + /** + * @deprecated use the new constructor that allows to set the fast forward mode. + * @param mergeRemote remote name used for merge + * @param mergeTarget remote branch to be merged into current branch + * @param mergeStrategy merge strategy to be used + */ + @Deprecated public UserMergeOptions(String mergeRemote, String mergeTarget, String mergeStrategy) { + this(mergeRemote, mergeTarget, mergeStrategy, MergeCommand.GitPluginFastForwardMode.FF); + } + + /** + * @param mergeRemote remote name used for merge + * @param mergeTarget remote branch to be merged into current branch + * @param mergeStrategy merge strategy + * @param fastForwardMode fast forward mode + */ + public UserMergeOptions(String mergeRemote, String mergeTarget, String mergeStrategy, + MergeCommand.GitPluginFastForwardMode fastForwardMode) { this.mergeRemote = mergeRemote; this.mergeTarget = mergeTarget; this.mergeStrategy = mergeStrategy; + this.fastForwardMode = fastForwardMode; } + @DataBoundConstructor + public UserMergeOptions(String mergeTarget) { + this.mergeTarget = mergeTarget; + } + + /** + * Construct UserMergeOptions from PreBuildMergeOptions. + * @param pbm pre-build merge options used to construct UserMergeOptions + */ public UserMergeOptions(PreBuildMergeOptions pbm) { - this(pbm.getRemoteBranchName(),pbm.getMergeTarget(),pbm.getMergeStrategy().toString()); + this(pbm.getRemoteBranchName(), pbm.getMergeTarget(), pbm.getMergeStrategy().toString(), pbm.getFastForwardMode()); } /** * Repository name, such as 'origin' that designates which repository the branch lives in. + * @return repository name */ + @Whitelisted public String getMergeRemote() { return mergeRemote; } + @DataBoundSetter + public void setMergeRemote(String mergeRemote) { + this.mergeRemote = Util.fixEmptyAndTrim(mergeRemote); + } + /** * Ref in the repository that becomes the input of the merge. * Normally a branch name like 'master'. + * @return branch name from which merge will be performed */ + @Whitelisted public String getMergeTarget() { return mergeTarget; } + /** + * Ref in the repository that becomes the input of the merge, a + * slash separated concatenation of merge remote and merge target. + * @return ref from which merge will be performed + */ public String getRef() { return mergeRemote + "/" + mergeTarget; } @@ -59,18 +107,73 @@ public MergeCommand.Strategy getMergeStrategy() { return MergeCommand.Strategy.DEFAULT; } + @DataBoundSetter + public void setMergeStrategy(MergeCommand.Strategy mergeStrategy) { + this.mergeStrategy = mergeStrategy.toString(); // not .name() as you might expect! TODO in Turkey this will be e.g. recursıve + } + + public MergeCommand.GitPluginFastForwardMode getFastForwardMode() { + for (MergeCommand.GitPluginFastForwardMode ffMode : MergeCommand.GitPluginFastForwardMode.values()) + if (ffMode.equals(fastForwardMode)) + return ffMode; + return MergeCommand.GitPluginFastForwardMode.FF; + } + + @DataBoundSetter + public void setFastForwardMode(MergeCommand.GitPluginFastForwardMode fastForwardMode) { + this.fastForwardMode = fastForwardMode; + } + + @Override + public String toString() { + return "UserMergeOptions{" + + "mergeRemote='" + mergeRemote + '\'' + + ", mergeTarget='" + mergeTarget + '\'' + + ", mergeStrategy='" + getMergeStrategy().name() + '\'' + + ", fastForwardMode='" + getFastForwardMode().name() + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UserMergeOptions that = (UserMergeOptions) o; + + return Objects.equals(mergeRemote, that.mergeRemote) + && Objects.equals(mergeTarget, that.mergeTarget) + && Objects.equals(mergeStrategy, that.mergeStrategy) + && Objects.equals(fastForwardMode, that.fastForwardMode); + } + + @Override + public int hashCode() { + return Objects.hash(mergeRemote, mergeTarget, mergeStrategy, fastForwardMode); + } + @Extension - public static class DescriptorImpl extends Descriptor { + public static class DescriptorImpl extends Descriptor implements CustomDescribableModel { + @Override public String getDisplayName() { return ""; } - public ListBoxModel doFillMergeStrategyItems() { - ListBoxModel m = new ListBoxModel(); - for (MergeCommand.Strategy strategy: MergeCommand.Strategy.values()) - m.add(strategy.toString(), strategy.toString()); - return m; + @Override + public Map customInstantiate(Map arguments) { + Map r = new HashMap<>(arguments); + Object mergeStrategy = r.get("mergeStrategy"); + if (mergeStrategy instanceof String) { + r.put("mergeStrategy", ((String) mergeStrategy).toUpperCase(Locale.ROOT)); + } + return r; } + } + } diff --git a/src/main/java/hudson/plugins/git/UserRemoteConfig.java b/src/main/java/hudson/plugins/git/UserRemoteConfig.java index c871db5903..72abac2a0c 100644 --- a/src/main/java/hudson/plugins/git/UserRemoteConfig.java +++ b/src/main/java/hudson/plugins/git/UserRemoteConfig.java @@ -4,14 +4,19 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.EnvVars; import hudson.Extension; import hudson.Util; import hudson.model.AbstractDescribableImpl; -import hudson.model.AbstractProject; +import hudson.model.Computer; import hudson.model.Descriptor; import hudson.model.Item; +import hudson.model.Job; +import hudson.model.Queue; import hudson.model.TaskListener; +import hudson.model.queue.Tasks; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; @@ -28,8 +33,14 @@ import java.io.IOException; import java.io.Serializable; import java.util.regex.Pattern; +import java.util.UUID; +import org.apache.commons.lang.StringUtils; -import static hudson.Util.*; +import static hudson.Util.fixEmpty; +import static hudson.Util.fixEmptyAndTrim; +import hudson.model.FreeStyleProject; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.kohsuke.stapler.interceptor.RequirePOST; @ExportedBean public class UserRemoteConfig extends AbstractDescribableImpl implements Serializable { @@ -40,7 +51,7 @@ public class UserRemoteConfig extends AbstractDescribableImpl private String credentialsId; @DataBoundConstructor - public UserRemoteConfig(String url, String name, String refspec, String credentialsId) { + public UserRemoteConfig(String url, String name, String refspec, @CheckForNull String credentialsId) { this.url = fixEmptyAndTrim(url); this.name = fixEmpty(name); this.refspec = fixEmpty(refspec); @@ -48,25 +59,32 @@ public UserRemoteConfig(String url, String name, String refspec, String credenti } @Exported + @Whitelisted public String getName() { return name; } @Exported + @Whitelisted public String getRefspec() { return refspec; } @Exported + @CheckForNull + @Whitelisted public String getUrl() { return url; } @Exported + @Whitelisted + @CheckForNull public String getCredentialsId() { return credentialsId; } + @Override public String toString() { return getRefspec() + " => " + getUrl() + " (" + getName() + ")"; } @@ -76,28 +94,40 @@ public String toString() { @Extension public static class DescriptorImpl extends Descriptor { - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath AbstractProject project, - @QueryParameter String url) { + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item project, + @QueryParameter String url, + @QueryParameter String credentialsId) { + if (project == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + project != null && !project.hasPermission(Item.EXTENDED_READ)) { + return new StandardListBoxModel().includeCurrentValue(credentialsId); + } + if (project == null) { + /* Construct a fake project */ + project = new FreeStyleProject(Jenkins.get(), "fake-" + UUID.randomUUID().toString()); + } return new StandardListBoxModel() - .withEmptySelection() - .withMatching( - GitClient.CREDENTIALS_MATCHER, - CredentialsProvider.lookupCredentials(StandardCredentials.class, - project, - ACL.SYSTEM, - GitURIRequirementsBuilder.fromUri(url).build()) - ); + .includeEmptyValue() + .includeMatchingAs( + project instanceof Queue.Task + ? Tasks.getAuthenticationOf((Queue.Task) project) + : ACL.SYSTEM, + project, + StandardUsernameCredentials.class, + GitURIRequirementsBuilder.fromUri(url).build(), + GitClient.CREDENTIALS_MATCHER) + .includeCurrentValue(credentialsId); } - public FormValidation doCheckCredentialsId(@AncestorInPath AbstractProject project, + public FormValidation doCheckCredentialsId(@AncestorInPath Item project, @QueryParameter String url, @QueryParameter String value) { - value = Util.fixEmptyAndTrim(value); - if (value == null) { + if (project == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + project != null && !project.hasPermission(Item.EXTENDED_READ)) { return FormValidation.ok(); } - if (!Jenkins.getInstance().hasPermission(Item.CONFIGURE)) { + value = Util.fixEmptyAndTrim(value); + if (value == null) { return FormValidation.ok(); } @@ -113,41 +143,64 @@ public FormValidation doCheckCredentialsId(@AncestorInPath AbstractProject proje { return FormValidation.ok(); } - - StandardCredentials credentials = lookupCredentials(project, value, url); - - if (credentials == null) { - // no credentials available, can't check - return FormValidation.warning("Cannot find any credentials with id " + value); + for (ListBoxModel.Option o : CredentialsProvider + .listCredentials(StandardUsernameCredentials.class, project, project instanceof Queue.Task + ? Tasks.getAuthenticationOf((Queue.Task) project) + : ACL.SYSTEM, + GitURIRequirementsBuilder.fromUri(url).build(), + GitClient.CREDENTIALS_MATCHER)) { + if (StringUtils.equals(value, o.value)) { + // TODO check if this type of credential is acceptable to the Git client or does it merit warning + // NOTE: we would need to actually lookup the credential to do the check, which may require + // fetching the actual credential instance from a remote credentials store. Perhaps this is + // not required + return FormValidation.ok(); + } } - - // TODO check if this type of credential is acceptible to the Git client or does it merit warning the user - - return FormValidation.ok(); + // no credentials available, can't check + return FormValidation.warning("Cannot find any credentials with id " + value); } - public FormValidation doCheckUrl(@AncestorInPath AbstractProject project, + @RequirePOST + public FormValidation doCheckUrl(@AncestorInPath Item item, @QueryParameter String credentialsId, @QueryParameter String value) throws IOException, InterruptedException { + // Normally this permission is hidden and implied by Item.CONFIGURE, so from a view-only form you will not be able to use this check. + // (TODO under certain circumstances being granted only USE_OWN might suffice, though this presumes a fix of JENKINS-31870.) + if (item == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + item != null && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return FormValidation.ok(); + } + String url = Util.fixEmptyAndTrim(value); if (url == null) - return FormValidation.error("Please enter Git repository."); + return FormValidation.error(Messages.UserRemoteConfig_CheckUrl_UrlIsNull()); if (url.indexOf('$') >= 0) // set by variable, can't validate return FormValidation.ok(); - if (!Jenkins.getInstance().hasPermission(Item.CONFIGURE)) - return FormValidation.ok(); - // get git executable on master - final EnvVars environment = new EnvVars(System.getenv()); // GitUtils.getPollEnvironment(project, null, launcher, TaskListener.NULL, false); + EnvVars environment; + Jenkins jenkins = Jenkins.get(); + if (item instanceof Job) { + environment = ((Job) item).getEnvironment(jenkins, TaskListener.NULL); + } else { + Computer computer = jenkins.toComputer(); + environment = computer == null ? new EnvVars() : computer.buildEnvironment(TaskListener.NULL); + } GitClient git = Git.with(TaskListener.NULL, environment) .using(GitTool.getDefaultInstallation().getGitExe()) .getClient(); - git.addDefaultCredentials(lookupCredentials(project, credentialsId, url)); + StandardCredentials credential = lookupCredentials(item, credentialsId, url); + git.addDefaultCredentials(credential); + + // Should not track credentials use in any checkURL method, rather should track + // credentials use at the point where the credential is used to perform an + // action (like poll the repository, clone the repository, publish a change + // to the repository). // attempt to connect the provided URL try { @@ -159,7 +212,7 @@ public FormValidation doCheckUrl(@AncestorInPath AbstractProject project, return FormValidation.ok(); } - private static StandardCredentials lookupCredentials(AbstractProject project, String credentialId, String uri) { + private static StandardCredentials lookupCredentials(@CheckForNull Item project, String credentialId, String uri) { return (credentialId == null) ? null : CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials(StandardCredentials.class, project, ACL.SYSTEM, GitURIRequirementsBuilder.fromUri(uri).build()), diff --git a/src/main/java/hudson/plugins/git/browser/AssemblaWeb.java b/src/main/java/hudson/plugins/git/browser/AssemblaWeb.java new file mode 100644 index 0000000000..a6627fae84 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/AssemblaWeb.java @@ -0,0 +1,126 @@ +package hudson.plugins.git.browser; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import hudson.scm.EditType; +import hudson.scm.RepositoryBrowser; +import hudson.util.FormValidation; +import hudson.util.FormValidation.URLCheck; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import java.io.IOException; +import java.net.URL; + +/** + * AssemblaWeb Git Browser URLs + */ +public class AssemblaWeb extends GitRepositoryBrowser { + + private static final long serialVersionUID = 1L; + + @DataBoundConstructor + public AssemblaWeb(String repoUrl) { + super(repoUrl); + } + + /** + * Creates a link to the change set + * http://[AssemblaWeb URL]/commits/[commit] + * + * @param changeSet commit hash + * @return change set link + * @throws IOException on input or output error + */ + @Override + public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { + URL url = getUrl(); + return new URL(url, url.getPath() + "commits/" + changeSet.getId()); + } + + /** + * Shows the difference between the referenced commit and the previous commit. + * The changes section also display diffs, so a separate url is unnecessary. + * http://[Assembla URL]/commits/[commit] + * + * @param path affected file path + * @return diff link + * @throws IOException on input or output error + */ + @Override + public URL getDiffLink(Path path) throws IOException { + GitChangeSet changeSet = path.getChangeSet(); + return getChangeSetLink(changeSet); + } + + /** + * Creates a link to the file. + * http://[Assembla URL]/nodes/[commit]/[path] + * + * @param path affected file path + * @return diff link + * @throws IOException on input or output error + */ + @Override + public URL getFileLink(Path path) throws IOException { + GitChangeSet changeSet = path.getChangeSet(); + URL url = getUrl(); + if (path.getEditType() == EditType.DELETE) { + return encodeURL(new URL(url, url.getPath() + "nodes/" + changeSet.getParentCommit() + path.getPath())); + } else { + return encodeURL(new URL(url, url.getPath() + "nodes/" + changeSet.getId() + path.getPath())); + } + } + + @Extension + public static class AssemblaWebDescriptor extends Descriptor> { + @Nonnull + public String getDisplayName() { + return "AssemblaWeb"; + } + + @Override + public AssemblaWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc + return req.bindJSON(AssemblaWeb.class, jsonObject); + } + + @RequirePOST + public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) final String url) + throws IOException, ServletException { + if (url == null) // nothing entered yet + { + return FormValidation.ok(); + } + // Connect to URL and check content only if we have admin permission + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) + return FormValidation.ok(); + return new URLCheck() { + protected FormValidation check() throws IOException, ServletException { + String v = url; + if (!v.endsWith("/")) { + v += '/'; + } + + try { + if (findText(open(new URL(v)), "Assembla")) { + return FormValidation.ok(); + } else { + return FormValidation.error("This is a valid URL but it doesn't look like Assembla"); + } + } catch (IOException e) { + return handleIOException(v, e); + } + } + }.check(); + } + } +} diff --git a/src/main/java/hudson/plugins/git/browser/BitbucketWeb.java b/src/main/java/hudson/plugins/git/browser/BitbucketWeb.java index acf7078335..c68d4fe0ed 100644 --- a/src/main/java/hudson/plugins/git/browser/BitbucketWeb.java +++ b/src/main/java/hudson/plugins/git/browser/BitbucketWeb.java @@ -5,13 +5,12 @@ import hudson.plugins.git.GitChangeSet; import hudson.scm.EditType; import hudson.scm.RepositoryBrowser; -import hudson.scm.browsers.QueryBuilder; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -38,7 +37,7 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(GitChangeSet.Path path) throws IOException { @@ -46,7 +45,6 @@ public URL getDiffLink(GitChangeSet.Path path) throws IOException { || path.getChangeSet().getParentCommit() == null) { return null; } - final String pathAsString = path.getPath(); return getDiffLinkRegardlessOfEditType(path); } @@ -62,23 +60,25 @@ private URL getDiffLinkRegardlessOfEditType(GitChangeSet.Path path) throws IOExc * * @param path file * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(GitChangeSet.Path path) throws IOException { final String pathAsString = path.getPath(); URL url = getUrl(); - return new URL(url, url.getPath() + "history/" + pathAsString); + return encodeURL(new URL(url, url.getPath() + "history/" + pathAsString)); } @Extension public static class BitbucketWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "bitbucketweb"; } @Override - public BitbucketWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public BitbucketWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(BitbucketWeb.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/CGit.java b/src/main/java/hudson/plugins/git/browser/CGit.java index 15e06922b8..6f1b251d80 100644 --- a/src/main/java/hudson/plugins/git/browser/CGit.java +++ b/src/main/java/hudson/plugins/git/browser/CGit.java @@ -11,8 +11,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -37,7 +37,7 @@ private QueryBuilder param(URL url) { * * @param changeSet commit hash * @return change set link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { @@ -51,7 +51,7 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { @@ -66,27 +66,29 @@ public URL getDiffLink(Path path) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { GitChangeSet changeSet = path.getChangeSet(); URL url = getUrl(); if (path.getEditType() == EditType.DELETE) { - return new URL(url, url.getPath() + "tree/" + path.getPath() + param(url).add("id=" + changeSet.getParentCommit()).toString()); + return encodeURL(new URL(url, url.getPath() + "tree/" + path.getPath() + param(url).add("id=" + changeSet.getParentCommit()).toString())); } else { - return new URL(url, url.getPath() + "tree/" + path.getPath() + param(url).add("id=" + changeSet.getId()).toString()); + return encodeURL(new URL(url, url.getPath() + "tree/" + path.getPath() + param(url).add("id=" + changeSet.getId()).toString())); } } @Extension public static class CGITDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "cgit"; } @Override - public CGit newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public CGit newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(CGit.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser.java b/src/main/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser.java index 056236de51..b59296f7fe 100644 --- a/src/main/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser.java +++ b/src/main/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser.java @@ -2,21 +2,22 @@ import hudson.Extension; import hudson.model.Descriptor; -import hudson.model.Hudson; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; import hudson.scm.EditType; import hudson.scm.RepositoryBrowser; import hudson.util.FormValidation; import hudson.util.FormValidation.URLCheck; +import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.regex.Pattern; @@ -40,7 +41,7 @@ public URL getDiffLink(Path path) throws IOException { @Override public URL getFileLink(Path path) throws IOException { - return new URL(getUrl(), getPath(path)); + return encodeURL(new URL(getUrl(), getPath(path))); } private String getPath(Path path) { @@ -67,19 +68,26 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { @Extension public static class FisheyeGitRepositoryBrowserDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "FishEye"; } @Override - public FisheyeGitRepositoryBrowser newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public FisheyeGitRepositoryBrowser newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(FisheyeGitRepositoryBrowser.class, jsonObject); } /** * Performs on-the-fly validation of the URL. + * @param value URL value to be checked + * @return form validation result + * @throws IOException on input or output error + * @throws ServletException on servlet error */ - public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) String value) throws IOException, + @RequirePOST + public FormValidation doCheckRepoUrl(@QueryParameter(fixEmpty = true) String value) throws IOException, ServletException { if (value == null) // nothing entered yet return FormValidation.ok(); @@ -87,10 +95,10 @@ public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) String value) if (!value.endsWith("/")) value += '/'; if (!URL_PATTERN.matcher(value).matches()) - return FormValidation.errorWithMarkup("The URL should end like .../browse/foobar/"); + return FormValidation.errorWithMarkup("The URL should end like .../browse/foobar/"); // Connect to URL and check content only if we have admin permission - if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) return FormValidation.ok(); final String finalValue = value; diff --git a/src/main/java/hudson/plugins/git/browser/GitBlitRepositoryBrowser.java b/src/main/java/hudson/plugins/git/browser/GitBlitRepositoryBrowser.java index 929f94405f..bfd8c4331b 100644 --- a/src/main/java/hudson/plugins/git/browser/GitBlitRepositoryBrowser.java +++ b/src/main/java/hudson/plugins/git/browser/GitBlitRepositoryBrowser.java @@ -8,15 +8,17 @@ import hudson.scm.RepositoryBrowser; import hudson.util.FormValidation; import hudson.util.FormValidation.URLCheck; +import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; @@ -66,21 +68,27 @@ private String encodeString(final String s) throws UnsupportedEncodingException } @Extension public static class ViewGitWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "gitblit"; } @Override - public GitBlitRepositoryBrowser newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public GitBlitRepositoryBrowser newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(GitBlitRepositoryBrowser.class, jsonObject); } + @RequirePOST public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) final String url) throws IOException, ServletException { if (url == null) // nothing entered yet { return FormValidation.ok(); } + // Connect to URL and check content only if we have admin permission + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) + return FormValidation.ok(); return new URLCheck() { protected FormValidation check() throws IOException, ServletException { String v = url; diff --git a/src/main/java/hudson/plugins/git/browser/GitLab.java b/src/main/java/hudson/plugins/git/browser/GitLab.java index 24206917cd..699810cdbc 100644 --- a/src/main/java/hudson/plugins/git/browser/GitLab.java +++ b/src/main/java/hudson/plugins/git/browser/GitLab.java @@ -6,14 +6,21 @@ import hudson.plugins.git.GitChangeSet.Path; import hudson.scm.EditType; import hudson.scm.RepositoryBrowser; +import hudson.util.FormValidation; import net.sf.json.JSONObject; + import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; + +import org.kohsuke.stapler.QueryParameter; + /** * Git Browser for GitLab */ @@ -21,90 +28,152 @@ public class GitLab extends GitRepositoryBrowser { private static final long serialVersionUID = 1L; - private final double version; + private Double version; + + private static double valueOfVersion(String version) throws NumberFormatException { + double tmpVersion = Double.valueOf(version); + if (Double.isNaN(tmpVersion)) { + throw new NumberFormatException("Version cannot be NaN (not a number)"); + } + if (Double.isInfinite(tmpVersion)) { + throw new NumberFormatException("Version cannot be infinite"); + } + return tmpVersion; + } @DataBoundConstructor + public GitLab(String repoUrl) { + super(repoUrl); + } + + @Deprecated public GitLab(String repoUrl, String version) { super(repoUrl); - this.version = Double.valueOf(version); + setVersion(version); } - public double getVersion() { - return version; + @DataBoundSetter + public void setVersion(String version) { + try { + this.version = valueOfVersion(version); + } catch (NumberFormatException nfe) { + // ignore + } + } + + public String getVersion() { + return (version != null) ? String.valueOf(version) : null; + } + + /* package */ + double getVersionDouble() { + return (version != null) ? version : Double.POSITIVE_INFINITY; } /** * Creates a link to the changeset * - * https://[GitLab URL]/commits/a9182a07750c9a0dfd89a8461adf72ef5ef0885b + * v < 3.0: [GitLab URL]/commits/[Hash] + * else: [GitLab URL]/commit/[Hash] * * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { - String commitPrefix; - - return new URL(getUrl(), calculatePrefix() + changeSet.getId().toString()); + return new URL(getUrl(), calculatePrefix() + changeSet.getId()); } /** * Creates a link to the commit diff. - * - * https://[GitLab URL]/commits/a9182a07750c9a0dfd89a8461adf72ef5ef0885b#[path to file] - * - * @param path + * + * v < 3.0: [GitLab URL]/commits/[Hash]#[File path] + * v < 8.0: [GitLab URL]/commit/[Hash]#[File path] + * else: [GitLab URL]/commit/[Hash]#diff-[index] + * + * @param path file path used in diff link * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); - return new URL(getUrl(), calculatePrefix() + changeSet.getId().toString() + "#" + path.getPath()); + String filelink; + if(getVersionDouble() < 8.0) { + filelink = "#" + path.getPath(); + } else + { + filelink = "#diff-" + String.valueOf(getIndexOfPath(path)); + } + return new URL(getUrl(), calculatePrefix() + changeSet.getId() + filelink); } /** * Creates a link to the file. - * https://[GitLab URL]/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/tree/pom.xml - * - * @param path + * v ≤ 4.2: [GitLab URL]tree/[Hash]/[File path] + * v < 5.1: [GitLab URL][Hash]/tree/[File path] + * else: [GitLab URL]blob/[Hash]/[File path] + * + * @param path file path used in diff link * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { if (path.getEditType().equals(EditType.DELETE)) { return getDiffLink(path); } else { - String spec; - if(getVersion() >= 5.1) { - spec = "blob/" + path.getChangeSet().getId() + "/" + path.getPath(); + if (getVersionDouble() <= 4.2) { + return encodeURL(new URL(getUrl(), "tree/" + path.getChangeSet().getId() + "/" + path.getPath())); + } else if (getVersionDouble() < 5.1) { + return encodeURL(new URL(getUrl(), path.getChangeSet().getId() + "/tree/" + path.getPath())); } else { - spec = path.getChangeSet().getId() + "/tree/" + path.getPath(); + return encodeURL(new URL(getUrl(), "blob/" + path.getChangeSet().getId() + "/" + path.getPath())); } - URL url = getUrl(); - return new URL(url, url.getPath() + spec); } } @Extension public static class GitLabDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "gitlab"; } @Override - public GitLab newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public GitLab newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(GitLab.class, jsonObject); } + + /** + * Validate the contents of the version field. + * + * @param version gitlab version value entered by the user + * @return validation result, either ok() or error(msg) + * @throws IOException on input or output error + * @throws ServletException on servlet error + */ + public FormValidation doCheckVersion(@QueryParameter(fixEmpty = true) final String version) + throws IOException, ServletException { + if (version == null) { + return FormValidation.ok(); + } + try { + valueOfVersion(version); + } catch (NumberFormatException nfe) { + return FormValidation.error("Can't convert '" + version + "' to a number: " + nfe.getMessage()); + } + return FormValidation.ok(); + } } private String calculatePrefix() { - if(getVersion() >= 3){ + if(getVersionDouble() < 3) { + return "commits/"; + } else { return "commit/"; } - - return "commits/"; - } + } } diff --git a/src/main/java/hudson/plugins/git/browser/GitList.java b/src/main/java/hudson/plugins/git/browser/GitList.java new file mode 100644 index 0000000000..8e62cdaaf8 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/GitList.java @@ -0,0 +1,97 @@ +package hudson.plugins.git.browser; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import hudson.scm.EditType; +import hudson.scm.RepositoryBrowser; +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.net.URL; + +/** + * Git Browser URLs + */ +public class GitList extends GitRepositoryBrowser { + + private static final long serialVersionUID = 1L; + + @DataBoundConstructor + public GitList(String repoUrl) { + super(repoUrl); + } + + @Override + public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { + URL url = getUrl(); + return new URL(url, url.getPath() + "commit/" + changeSet.getId()); + } + + /** + * Creates a link to the file diff. + * http://[GitList URL]/commit/6c99ffee4cb6d605d55a1cc7cb47f25a443f7f54#N + * + * @param path affected file path + * @return diff link + * @throws IOException on I/O error + */ + @Override + public URL getDiffLink(Path path) throws IOException { + if(path.getEditType() != EditType.EDIT || path.getSrc() == null || path.getDst() == null + || path.getChangeSet().getParentCommit() == null) { + return null; + } + return getDiffLinkRegardlessOfEditType(path); + } + + /** + * Return a diff link regardless of the edit type by appending the index of the pathname in the changeset. + * + * @param path file path used in diff link + * @return url for differences + * @throws IOException on input or output error + */ + private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { + //GitList diff indices begin at 1 + return encodeURL(new URL(getChangeSetLink(path.getChangeSet()), "#" + String.valueOf(getIndexOfPath(path) + 1))); + } + + /** + * Creates a link to the file. + * http://[GitList URL]/blob/6c99ffee4cb6d605d55a1cc7cb47f25a443f7f54/src/gitlist/Application.php + * + * @param path file + * @return file link + * @throws IOException on input or output error + */ + @Override + public URL getFileLink(Path path) throws IOException { + if (path.getEditType().equals(EditType.DELETE)) { + return getDiffLinkRegardlessOfEditType(path); + } else { + final String spec = "blob/" + path.getChangeSet().getId() + "/" + path.getPath(); + URL url = getUrl(); + return encodeURL(new URL(url, url.getPath() + spec)); + } + } + + @Extension + public static class GitListDescriptor extends Descriptor> { + @Nonnull + public String getDisplayName() { + return "gitlist"; + } + + @Override + public GitList newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc + return req.bindJSON(GitList.class, jsonObject); + } + } +} diff --git a/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java b/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java index 115b8e76c4..e23661756c 100644 --- a/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java +++ b/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java @@ -4,11 +4,16 @@ import hudson.model.Job; import hudson.model.TaskListener; import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; import hudson.scm.RepositoryBrowser; + import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; +import java.net.IDN; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; public abstract class GitRepositoryBrowser extends RepositoryBrowser { @@ -33,7 +38,7 @@ public final URL getUrl() throws IOException { if (req != null) { Job job = req.findAncestorObject(Job.class); if (job != null) { - EnvVars env = null; + EnvVars env; try { env = job.getEnvironment(null, TaskListener.NULL); } catch (InterruptedException e) { @@ -58,7 +63,7 @@ public final URL getUrl() throws IOException { * @param path affected file path * @return * null if the browser doesn't have any URL for diff. - * @throws IOException + * @throws IOException on input or output error */ public abstract URL getDiffLink(GitChangeSet.Path path) throws IOException; @@ -69,9 +74,10 @@ public final URL getUrl() throws IOException { * @param path affected file path * @return * null if the browser doesn't have any suitable URL. - * @throws IOException + * @throws IOException on input or output error + * @throws URISyntaxException on URI syntax error */ - public abstract URL getFileLink(GitChangeSet.Path path) throws IOException; + public abstract URL getFileLink(GitChangeSet.Path path) throws IOException, URISyntaxException; /** * Determines whether a URL should be normalized @@ -82,6 +88,34 @@ public final URL getUrl() throws IOException { protected boolean getNormalizeUrl() { return true; } + + /** + * Calculate the index of the given path in a + * sorted list of affected files + * + * @param path affected file path + * @return The index in the lexicographical sorted filelist + * @throws IOException on input or output error + */ + protected int getIndexOfPath(Path path) throws IOException { + final String pathAsString = path.getPath(); + final GitChangeSet changeSet = path.getChangeSet(); + int i = 0; + for (String affected : changeSet.getAffectedPaths()) + { + if (affected.compareTo(pathAsString) < 0) + i++; + } + return i; + } + + public static URL encodeURL(URL url) throws IOException { + try { + return new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), url.getPath(), url.getQuery(), url.getRef()).toURL(); + } catch (URISyntaxException e) { + throw new IOException(e); + } + } private static final long serialVersionUID = 1L; } diff --git a/src/main/java/hudson/plugins/git/browser/GitWeb.java b/src/main/java/hudson/plugins/git/browser/GitWeb.java index d649270d6c..e1ad5e23d0 100644 --- a/src/main/java/hudson/plugins/git/browser/GitWeb.java +++ b/src/main/java/hudson/plugins/git/browser/GitWeb.java @@ -11,8 +11,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -49,7 +49,7 @@ private QueryBuilder param(URL url) { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { @@ -70,7 +70,7 @@ public URL getDiffLink(Path path) throws IOException { * http://[GitWeb URL]?a=blob;f=[path];h=[dst, or src for deleted files];hb=[commit] * @param path file * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -78,17 +78,19 @@ public URL getFileLink(Path path) throws IOException { String h = (path.getDst() != null) ? path.getDst() : path.getSrc(); String spec = param(url).add("a=blob").add("f=" + path.getPath()) .add("h=" + h).add("hb=" + path.getChangeSet().getId()).toString(); - return new URL(url, url.getPath()+spec); + return encodeURL(new URL(url, url.getPath()+spec)); } @Extension public static class GitWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "gitweb"; } @Override - public GitWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public GitWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(GitWeb.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/GithubWeb.java b/src/main/java/hudson/plugins/git/browser/GithubWeb.java index 312d4ddbbf..228faa19a9 100644 --- a/src/main/java/hudson/plugins/git/browser/GithubWeb.java +++ b/src/main/java/hudson/plugins/git/browser/GithubWeb.java @@ -1,27 +1,19 @@ package hudson.plugins.git.browser; -import hudson.EnvVars; import hudson.Extension; -import hudson.model.AbstractProject; import hudson.model.Descriptor; -import hudson.model.EnvironmentContributor; -import hudson.model.ItemGroup; -import hudson.model.Job; -import hudson.model.TaskListener; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; import hudson.scm.EditType; import hudson.scm.RepositoryBrowser; import net.sf.json.JSONObject; -import org.kohsuke.stapler.AncestorInPath; + import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; /** * Git Browser URLs @@ -38,7 +30,7 @@ public GithubWeb(String repoUrl) { @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { URL url = getUrl(); - return new URL(url, url.getPath()+"commit/" + changeSet.getId().toString()); + return new URL(url, url.getPath()+"commit/" + changeSet.getId()); } /** @@ -47,7 +39,7 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { @@ -61,19 +53,13 @@ public URL getDiffLink(Path path) throws IOException { /** * Return a diff link regardless of the edit type by appending the index of the pathname in the changeset. * - * @param path + * @param path file path used in diff link * @return url for differences - * @throws IOException + * @throws IOException on input or output error */ private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { - final GitChangeSet changeSet = path.getChangeSet(); - final ArrayList affectedPaths = new ArrayList(changeSet.getAffectedPaths()); - // Github seems to sort the output alphabetically by the path. - Collections.sort(affectedPaths); - final String pathAsString = path.getPath(); - final int i = Collections.binarySearch(affectedPaths, pathAsString); - assert i >= 0; - return new URL(getChangeSetLink(changeSet), "#diff-" + String.valueOf(i)); + // Github seems to sort the output alphabetically by the path. + return new URL(getChangeSetLink(path.getChangeSet()), "#diff-" + String.valueOf(getIndexOfPath(path))); } /** @@ -84,7 +70,7 @@ private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { * * @param path file * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -92,19 +78,25 @@ public URL getFileLink(Path path) throws IOException { return getDiffLinkRegardlessOfEditType(path); } else { final String spec = "blob/" + path.getChangeSet().getId() + "/" + path.getPath(); - URL url = getUrl(); - return new URL(url, url.getPath() + spec); + return encodeURL(buildURL(spec)); } } + private URL buildURL(String spec) throws IOException { + URL url = getUrl(); + return new URL(url, url.getPath() + spec); + } + @Extension public static class GithubWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "githubweb"; } @Override - public GithubWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public GithubWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(GithubWeb.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/Gitiles.java b/src/main/java/hudson/plugins/git/browser/Gitiles.java new file mode 100644 index 0000000000..9768e4b1f2 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/Gitiles.java @@ -0,0 +1,99 @@ +package hudson.plugins.git.browser; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import hudson.scm.RepositoryBrowser; +import hudson.util.FormValidation; +import hudson.util.FormValidation.URLCheck; + +import jenkins.model.Jenkins; + +import java.io.IOException; +import java.net.URL; + +import javax.annotation.Nonnull; +import javax.servlet.ServletException; + +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +/** + * @author Manolo Carrasco Moñino + */ +public class Gitiles extends GitRepositoryBrowser { + + private static final long serialVersionUID = 1L; + + @DataBoundConstructor + public Gitiles(String repoUrl) { + super(repoUrl); + } + + // https://gwt.googlesource.com/gwt/+/d556b611fef6df7bfe07682262b02309e6d41769%5E%21/#F3 + @Override + public URL getDiffLink(Path path) throws IOException { + URL url = getUrl(); + return new URL(url + "+/" + path.getChangeSet().getId() + "%5E%21"); + } + + // https://gwt.googlesource.com/gwt/+blame/d556b611fef6df7bfe07682262b02309e6d41769/dev/codeserver/java/com/google/gwt/dev/codeserver/ModuleState.java + @Override + public URL getFileLink(Path path) throws IOException { + URL url = getUrl(); + return encodeURL(new URL(url + "+blame/" + path.getChangeSet().getId() + "/" + path.getPath())); + } + + @Override + public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { + URL url = getUrl(); + return new URL(url + "+/" + changeSet.getId() + "%5E%21"); + } + + + @Extension + public static class ViewGitWebDescriptor extends Descriptor> { + @Nonnull + public String getDisplayName() { + return "gitiles"; + } + + @Override + public Gitiles newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc + return req.bindJSON(Gitiles.class, jsonObject); + } + + @RequirePOST + public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) final String url) throws IOException, ServletException { + if (url == null) // nothing entered yet + return FormValidation.ok(); + // Connect to URL and check content only if we have admin permission + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) + return FormValidation.ok(); + return new URLCheck() { + protected FormValidation check() throws IOException, ServletException { + String v = url; + if (!v.endsWith("/")) + v += '/'; + + try { + // gitiles has a line in main page indicating how to clone the project + if (findText(open(new URL(v)), "git clone")) { + return FormValidation.ok(); + } else { + return FormValidation.error("This is a valid URL but it doesn't look like Gitiles"); + } + } catch (IOException e) { + return handleIOException(v, e); + } + } + }.check(); + } + } +} diff --git a/src/main/java/hudson/plugins/git/browser/GitoriousWeb.java b/src/main/java/hudson/plugins/git/browser/GitoriousWeb.java index d99e5610ea..b0bebcd51c 100644 --- a/src/main/java/hudson/plugins/git/browser/GitoriousWeb.java +++ b/src/main/java/hudson/plugins/git/browser/GitoriousWeb.java @@ -10,8 +10,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -28,31 +28,31 @@ public GitoriousWeb(String repoUrl) { @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { - return new URL(getUrl(), "commit/" + changeSet.getId().toString()); + return new URL(getUrl(), "commit/" + changeSet.getId()); } /** * Creates a link to the commit diff. * - * https://[Gitorious URL]/commit/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/diffs?diffmode=sidebyside&fragment=1#[path to file] + * {@code https://[Gitorious URL]/commit/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/diffs?diffmode=sidebyside&fragment=1#[path to file]} * - * @param path + * @param path file path used in diff link * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); - return new URL(getUrl(), "commit/" + changeSet.getId().toString() + "/diffs?diffmode=sidebyside&fragment=1#" + path.getPath()); + return encodeURL(new URL(getUrl(), "commit/" + changeSet.getId() + "/diffs?diffmode=sidebyside&fragment=1#" + path.getPath())); } /** * Creates a link to the file. - * https://[Gitorious URL]/blobs/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/pom.xml + * {@code https://[Gitorious URL]/blobs/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/pom.xml} * - * @param path + * @param path file path used in diff link * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -67,12 +67,14 @@ public URL getFileLink(Path path) throws IOException { @Extension public static class GitoriousWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "gitoriousweb"; } @Override - public GitoriousWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public GitoriousWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(GitoriousWeb.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/GogsGit.java b/src/main/java/hudson/plugins/git/browser/GogsGit.java new file mode 100644 index 0000000000..8755b214c5 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/GogsGit.java @@ -0,0 +1,105 @@ +package hudson.plugins.git.browser; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import hudson.scm.EditType; +import hudson.scm.RepositoryBrowser; +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.net.URL; + +/** + * @author Norbert Lange (nolange79@gmail.com) + */ +public class GogsGit extends GitRepositoryBrowser { + + private static final long serialVersionUID = 1L; + + @DataBoundConstructor + public GogsGit(String repoUrl) { + super(repoUrl); + } + + /** + * Creates a link to the change set + * http://[GogsGit URL]/commit/[commit] + * + * @param changeSet commit hash + * @return change set link + * @throws IOException on input or output error + */ + @Override + public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { + URL url = getUrl(); + return new URL(url, url.getPath() + "commit/" + changeSet.getId()); + } + + /** + * Creates a link to the file diff. + * http://[GogsGit URL]/commit/[commit]#diff-N + * + * @param path affected file path + * @return diff link + * @throws IOException on input or output error + */ + @Override + public URL getDiffLink(Path path) throws IOException { + if (path.getEditType() != EditType.EDIT || path.getSrc() == null || path.getDst() == null + || path.getChangeSet().getParentCommit() == null) { + return null; + } + return getDiffLinkRegardlessOfEditType(path); + } + + /** + * Return a diff link regardless of the edit type by appending the index of the pathname in the changeset. + * + * @param path file path used in diff link + * @return url for differences + * @throws IOException on input or output error + */ + private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { + // Gogs diff indices begin at 1. + return encodeURL(new URL(getChangeSetLink(path.getChangeSet()), "#diff-" + String.valueOf(getIndexOfPath(path) + 1))); + } + + /** + * Creates a link to the file. + * http://[GogsGit URL]/src/[commit]/[path] + * Deleted Files link to the parent version. No easy way to find it + * + * @param path affected file path + * @return diff link + * @throws IOException on input or output error + */ + @Override + public URL getFileLink(Path path) throws IOException { + if (path.getEditType().equals(EditType.DELETE)) { + return getDiffLinkRegardlessOfEditType(path); + } else { + URL url = getUrl(); + return encodeURL(new URL(url, url.getPath() + "src/" + path.getChangeSet().getId() + "/" + path.getPath())); + } + } + + @Extension + public static class GogsGitDescriptor extends Descriptor> { + @Nonnull + public String getDisplayName() { + return "gogs"; + } + + @Override + public GogsGit newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc + return req.bindJSON(GogsGit.class, jsonObject); + } + } +} diff --git a/src/main/java/hudson/plugins/git/browser/KilnGit.java b/src/main/java/hudson/plugins/git/browser/KilnGit.java index d50954ca8d..c0745663fc 100644 --- a/src/main/java/hudson/plugins/git/browser/KilnGit.java +++ b/src/main/java/hudson/plugins/git/browser/KilnGit.java @@ -8,13 +8,13 @@ import hudson.scm.RepositoryBrowser; import hudson.scm.browsers.QueryBuilder; import net.sf.json.JSONObject; + import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; /** * @author Chris Klaiber (cklaiber@gmail.com) @@ -38,12 +38,12 @@ private QueryBuilder param(URL url) { * * @param changeSet commit hash * @return change set link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { URL url = getUrl(); - return new URL(url, url.getPath() + "History/" + changeSet.getId() + param(url).toString()); + return encodeURL(new URL(url, url.getPath() + "History/" + changeSet.getId() + param(url).toString())); } /** @@ -52,7 +52,7 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { @@ -66,17 +66,13 @@ public URL getDiffLink(Path path) throws IOException { /** * Return a diff link regardless of the edit type by appending the index of the pathname in the changeset. * - * @param path + * @param path file path used in diff link * @return url for differences - * @throws IOException + * @throws IOException on input or output error */ private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); - final ArrayList affectedPaths = new ArrayList(changeSet.getAffectedPaths()); - // Kiln seems to sort the output alphabetically by the path. - Collections.sort(affectedPaths); - final String pathAsString = path.getPath(); - final int i = Collections.binarySearch(affectedPaths, pathAsString); + final int i = getIndexOfPath(path); if (i >= 0) { // Kiln diff indices begin at 1. URL url = getUrl(); @@ -91,7 +87,7 @@ private URL getDiffLinkRegardlessOfEditType(Path path) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -100,18 +96,20 @@ public URL getFileLink(Path path) throws IOException { } else { GitChangeSet changeSet = path.getChangeSet(); URL url = getUrl(); - return new URL(url, url.getPath() + "FileHistory/" + path.getPath() + param(url).add("rev=" + changeSet.getId()).toString()); + return encodeURL(new URL(url, url.getPath() + "FileHistory/" + path.getPath() + param(url).add("rev=" + changeSet.getId()).toString())); } } @Extension public static class KilnGitDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "Kiln"; } @Override - public KilnGit newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public KilnGit newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(KilnGit.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/Phabricator.java b/src/main/java/hudson/plugins/git/browser/Phabricator.java index fdd80ca421..8aa198fa2e 100644 --- a/src/main/java/hudson/plugins/git/browser/Phabricator.java +++ b/src/main/java/hudson/plugins/git/browser/Phabricator.java @@ -4,14 +4,13 @@ import hudson.model.Descriptor; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; -import hudson.scm.EditType; import hudson.scm.RepositoryBrowser; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -39,11 +38,11 @@ public String getRepo() { * https://[Phabricator URL]/r$repo$sha * * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { - return new URL(getUrl(), String.format("/r%s%s", this.getRepo(), changeSet.getId().toString())); + return new URL(getUrl(), String.format("/r%s%s", this.getRepo(), changeSet.getId())); } /** @@ -52,14 +51,14 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * https://[Phabricator URL]/commits/a9182a07750c9a0dfd89a8461adf72ef5ef0885b#[path to file] * * - * @param path + * @param path file path used in diff link * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); - final String sha = changeSet.getId().toString(); + final String sha = changeSet.getId(); final String spec = String.format("/diffusion/%s/change/master/%s;%s", this.getRepo(), path.getPath(), sha); return new URL(getUrl(), spec); } @@ -68,26 +67,28 @@ public URL getDiffLink(Path path) throws IOException { * Creates a link to the file. * https://[Phabricator URL]/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/tree/pom.xml * - * @param path + * @param path file path used in diff link * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); - final String sha = changeSet.getId().toString(); + final String sha = changeSet.getId(); final String spec = String.format("/diffusion/%s/history/master/%s;%s", this.getRepo(), path.getPath(), sha); - return new URL(getUrl(), spec); + return encodeURL(new URL(getUrl(), spec)); } @Extension public static class PhabricatorDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "phabricator"; } @Override - public Phabricator newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public Phabricator newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(Phabricator.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/RedmineWeb.java b/src/main/java/hudson/plugins/git/browser/RedmineWeb.java index 0297411e21..53b6d17a23 100644 --- a/src/main/java/hudson/plugins/git/browser/RedmineWeb.java +++ b/src/main/java/hudson/plugins/git/browser/RedmineWeb.java @@ -10,8 +10,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -31,7 +31,7 @@ public RedmineWeb(String repoUrl) { @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { URL url = getUrl(); - return new URL(url, "diff?rev=" + changeSet.getId().toString()); + return new URL(url, "diff?rev=" + changeSet.getId()); } /** @@ -46,13 +46,13 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { * @param path * affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { final GitChangeSet changeSet = path.getChangeSet(); URL url = getUrl(); - final URL changeSetLink = new URL(url, "revisions/" + changeSet.getId().toString()); + final URL changeSetLink = new URL(url, "revisions/" + changeSet.getId()); final URL difflink; if (path.getEditType().equals(EditType.ADD)) { difflink = getFileLink(path); @@ -67,30 +67,31 @@ public URL getDiffLink(Path path) throws IOException { * https://SERVER/PATH/projects/PROJECT/repository/revisions/a9182a07750c9a0dfd89a8461adf72ef5ef0885b/entry/pom.xml * For deleted files just returns a diff link, which will have /dev/null as target file. * - * @param path - * file + * @param path affected file path * @return file link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { if (path.getEditType().equals(EditType.DELETE)) { - return getDiffLink(path); + return encodeURL(getDiffLink(path)); } else { final String spec = "revisions/" + path.getChangeSet().getId() + "/entry/" + path.getPath(); URL url = getUrl(); - return new URL(url, url.getPath() + spec); + return encodeURL(new URL(url, url.getPath() + spec)); } } @Extension public static class RedmineWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "redmineweb"; } @Override - public RedmineWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public RedmineWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(RedmineWeb.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/RhodeCode.java b/src/main/java/hudson/plugins/git/browser/RhodeCode.java index 5a3f48dd69..b058c308ae 100644 --- a/src/main/java/hudson/plugins/git/browser/RhodeCode.java +++ b/src/main/java/hudson/plugins/git/browser/RhodeCode.java @@ -11,8 +11,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -33,11 +33,11 @@ private QueryBuilder param(URL url) { /** * Creates a link to the change set - * http://[RhodeCode URL]/changeset/[commit] + * {@code http://[RhodeCode URL]/changeset/[commit]} * * @param changeSet commit hash * @return change set link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { @@ -47,31 +47,26 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { /** * Creates a link to the file diff. - * http://[RhodeCode URL]/diff/[path]?diff2=[commit]&diff1=[commit]&diff=diff+to+revision + * {@code http://[RhodeCode URL]/diff/[path]?diff2=[commit]&diff1=[commit]&diff=diff+to+revision} * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { GitChangeSet changeSet = path.getChangeSet(); URL url = getUrl(); - - if (path.getEditType() == EditType.DELETE) { - return new URL(url, url.getPath() + "diff/" + path.getPath() + param(url).add("diff2=" + changeSet.getParentCommit()).add("diff1=" + changeSet.getId()).toString() + "&diff=diff+to+revision"); - } else { - return new URL(url, url.getPath() + "diff/" + path.getPath() + param(url).add("diff2=" + changeSet.getId()).add("diff1=" + changeSet.getId()).toString() + "&diff=diff+to+revision"); - } + return new URL(url, url.getPath() + "diff/" + path.getPath() + param(url).add("diff2=" + changeSet.getParentCommit()).add("diff1=" + changeSet.getId()).toString() + "&diff=diff+to+revision"); } /** * Creates a link to the file. - * http://[RhodeCode URL]/files/[commit]/[path] + * {@code http://[RhodeCode URL]/files/[commit]/[path]} * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -79,20 +74,26 @@ public URL getFileLink(Path path) throws IOException { URL url = getUrl(); if (path.getEditType() == EditType.DELETE) { - return new URL(url, url.getPath() + "files/" + changeSet.getParentCommit().toString() + '/' + path.getPath()); + String parentCommit = changeSet.getParentCommit(); + if (parentCommit == null) { + parentCommit = "."; + } + return encodeURL(new URL(url, url.getPath() + "files/" + parentCommit + '/' + path.getPath())); } else { - return new URL(url, url.getPath() + "files/" + changeSet.getId().toString() + '/' + path.getPath()); + return encodeURL(new URL(url, url.getPath() + "files/" + changeSet.getId() + '/' + path.getPath())); } } @Extension public static class RhodeCodeDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "rhodecode"; } @Override - public RhodeCode newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public RhodeCode newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(RhodeCode.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/Stash.java b/src/main/java/hudson/plugins/git/browser/Stash.java index b739a9e9bb..0fa35a938c 100644 --- a/src/main/java/hudson/plugins/git/browser/Stash.java +++ b/src/main/java/hudson/plugins/git/browser/Stash.java @@ -11,8 +11,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; /** @@ -37,7 +37,7 @@ private QueryBuilder param(URL url) { * * @param changeSet commit hash * @return change set link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { @@ -47,11 +47,11 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { /** * Creates a link to the file diff. - * http://[Stash URL]/diff/[path]?at=[commit]&until=[commit] + * {@code http://[Stash URL]/diff/[path]?at=[commit]&until=[commit]} * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getDiffLink(Path path) throws IOException { @@ -71,7 +71,7 @@ public URL getDiffLink(Path path) throws IOException { * * @param path affected file path * @return diff link - * @throws IOException + * @throws IOException on input or output error */ @Override public URL getFileLink(Path path) throws IOException { @@ -79,20 +79,22 @@ public URL getFileLink(Path path) throws IOException { URL url = getUrl(); if (path.getEditType() == EditType.DELETE) { - return new URL(url, url.getPath() + "browse/" + path.getPath() + param(url).add("at=" + changeSet.getParentCommit()).toString()); + return encodeURL(new URL(url, url.getPath() + "browse/" + path.getPath() + param(url).add("at=" + changeSet.getParentCommit()).toString())); } else { - return new URL(url, url.getPath() + "browse/" + path.getPath() + param(url).add("at=" + changeSet.getId()).toString()); + return encodeURL(new URL(url, url.getPath() + "browse/" + path.getPath() + param(url).add("at=" + changeSet.getId()).toString())); } } @Extension public static class StashDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "stash"; } @Override - public Stash newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public Stash newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(Stash.class, jsonObject); } } diff --git a/src/main/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser.java b/src/main/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser.java new file mode 100644 index 0000000000..796945e1b1 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser.java @@ -0,0 +1,155 @@ +package hudson.plugins.git.browser; + +import hudson.Extension; +import hudson.model.AbstractProject; +import hudson.model.Descriptor; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitSCM; +import hudson.scm.RepositoryBrowser; +import hudson.util.FormValidation; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.transport.RemoteConfig; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Pattern; + +/** + * Browser for Git repositories on Microsoft Team Foundation Server (TFS) 2013 and higher versions using the + * same format. This includes Git repositories hosted with the Visual Studio Team Services. + */ +public class TFS2013GitRepositoryBrowser extends GitRepositoryBrowser { + + @DataBoundConstructor + public TFS2013GitRepositoryBrowser(String repoUrl) { + super(repoUrl); + } + + @Override + public URL getDiffLink(GitChangeSet.Path path) throws IOException { + String spec = String.format("commit/%s#path=%s&_a=compare", path.getChangeSet().getId(), path.getPath()); + return new URL(getRepoUrl(path.getChangeSet()), spec); + } + + @Override + public URL getFileLink(GitChangeSet.Path path) throws IOException { + String spec = String.format("commit/%s#path=%s&_a=history", path.getChangeSet().getId(), path.getPath()); + return encodeURL(new URL(getRepoUrl(path.getChangeSet()), spec)); + } + + @Override + public URL getChangeSetLink(GitChangeSet gitChangeSet) throws IOException { + return new URL(getRepoUrl(gitChangeSet), "commit/" + gitChangeSet.getId()); + } + + /*default*/ URL getRepoUrl(GitChangeSet changeSet) throws IOException { // default visibility for tests + String result = getRepoUrl(); + + if (StringUtils.isBlank(result)) + return normalizeToEndWithSlash(getUrlFromFirstConfiguredRepository(changeSet)); + + else if (!result.contains("/")) + return normalizeToEndWithSlash(getResultFromNamedRepository(changeSet)); + + return getUrl(); + } + + private URL getResultFromNamedRepository(GitChangeSet changeSet) throws MalformedURLException { + GitSCM scm = getScmFromProject(changeSet); + return new URL(scm.getRepositoryByName(getRepoUrl()).getURIs().get(0).toString()); + } + + private URL getUrlFromFirstConfiguredRepository(GitChangeSet changeSet) throws MalformedURLException { + GitSCM scm = getScmFromProject(changeSet); + return new URL(scm.getRepositories().get(0).getURIs().get(0).toString()); + } + + private GitSCM getScmFromProject(GitChangeSet changeSet) { + AbstractProject build = (AbstractProject) changeSet.getParent().getRun().getParent(); + + return (GitSCM) build.getScm(); + } + + @Extension + public static class TFS2013GitRepositoryBrowserDescriptor extends Descriptor> { + + private static final String REPOSITORY_BROWSER_LABEL = "Microsoft Team Foundation Server/Visual Studio Team Services"; + @Nonnull + public String getDisplayName() { + return REPOSITORY_BROWSER_LABEL; + } + + @Override + public TFS2013GitRepositoryBrowser newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc + try { + req.getSubmittedForm(); + } catch (ServletException e) { + e.printStackTrace(); + } + return req.bindJSON(TFS2013GitRepositoryBrowser.class, jsonObject); + } + + /** + * Performs on-the-fly validation of the URL. + * @param value URL value to be checked + * @param project project context used for check + * @return form validation result + * @throws IOException on input or output error + * @throws ServletException on servlet error + */ + @RequirePOST + public FormValidation doCheckRepoUrl(@QueryParameter(fixEmpty = true) String value, @AncestorInPath AbstractProject project) throws IOException, + ServletException { + + // Connect to URL and check content only if we have admin permission + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) + return FormValidation.ok(); + + if (value == null) // nothing entered yet + value = "origin"; + + if (!value.contains("/") && project != null) { + GitSCM scm = (GitSCM) project.getScm(); + RemoteConfig remote = scm.getRepositoryByName(value); + if (remote == null) + return FormValidation.errorWithMarkup("There is no remote with the name " + value + ""); + + value = remote.getURIs().get(0).toString(); + } + + if (!value.endsWith("/")) + value += '/'; + if (!URL_PATTERN.matcher(value).matches()) + return FormValidation.errorWithMarkup("The URL should end like .../_git/foobar/"); + + final String finalValue = value; + return new FormValidation.URLCheck() { + @Override + protected FormValidation check() throws IOException, ServletException { + try { + if (findText(open(new URL(finalValue)), REPOSITORY_BROWSER_LABEL)) { + return FormValidation.ok(); + } else { + return FormValidation.error("This is a valid URL but it doesn't look like Microsoft TFS 2013"); + } + } catch (IOException e) { + return handleIOException(finalValue, e); + } + } + }.check(); + } + + private static final Pattern URL_PATTERN = Pattern.compile(".+/_git/[^/]+/"); + } +} diff --git a/src/main/java/hudson/plugins/git/browser/ViewGitWeb.java b/src/main/java/hudson/plugins/git/browser/ViewGitWeb.java index e974dc0f36..ad3c3cafa3 100644 --- a/src/main/java/hudson/plugins/git/browser/ViewGitWeb.java +++ b/src/main/java/hudson/plugins/git/browser/ViewGitWeb.java @@ -9,15 +9,17 @@ import hudson.scm.browsers.QueryBuilder; import hudson.util.FormValidation; import hudson.util.FormValidation.URLCheck; +import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; @@ -48,15 +50,15 @@ public URL getFileLink(Path path) throws IOException { URL url = getUrl(); if (path.getEditType() == EditType.DELETE) { String spec = buildCommitDiffSpec(url, path); - return new URL(url, url.getPath() + spec); + return encodeURL(new URL(url, url.getPath() + spec)); } String spec = param(url).add("p=" + projectName).add("a=viewblob").add("h=" + path.getDst()).add("f=" + path.getPath()).toString(); - return new URL(url, url.getPath() + spec); + return encodeURL(new URL(url, url.getPath() + spec)); } private String buildCommitDiffSpec(URL url, Path path) throws UnsupportedEncodingException { - return param(url).add("p=" + projectName).add("a=commitdiff").add("h=" + path.getChangeSet().getId()).toString() + "#" + URLEncoder.encode(path.getPath(),"UTF-8").toString(); + return param(url).add("p=" + projectName).add("a=commitdiff").add("h=" + path.getChangeSet().getId()) + "#" + URLEncoder.encode(path.getPath(),"UTF-8").toString(); } @Override @@ -75,18 +77,24 @@ public String getProjectName() { @Extension public static class ViewGitWebDescriptor extends Descriptor> { + @Nonnull public String getDisplayName() { return "viewgit"; } @Override - public ViewGitWeb newInstance(StaplerRequest req, JSONObject jsonObject) throws FormException { + public ViewGitWeb newInstance(StaplerRequest req, @Nonnull JSONObject jsonObject) throws FormException { + assert req != null; //see inherited javadoc return req.bindJSON(ViewGitWeb.class, jsonObject); } + @RequirePOST public FormValidation doCheckUrl(@QueryParameter(fixEmpty = true) final String url) throws IOException, ServletException { if (url == null) // nothing entered yet return FormValidation.ok(); + // Connect to URL and check content only if we have admin permission + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) + return FormValidation.ok(); return new URLCheck() { protected FormValidation check() throws IOException, ServletException { String v = url; diff --git a/src/main/java/hudson/plugins/git/browser/casc/GitLabConfigurator.java b/src/main/java/hudson/plugins/git/browser/casc/GitLabConfigurator.java new file mode 100644 index 0000000000..5db0dfab57 --- /dev/null +++ b/src/main/java/hudson/plugins/git/browser/casc/GitLabConfigurator.java @@ -0,0 +1,56 @@ +package hudson.plugins.git.browser.casc; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.plugins.git.browser.GitLab; +import io.jenkins.plugins.casc.BaseConfigurator; +import io.jenkins.plugins.casc.ConfigurationContext; +import io.jenkins.plugins.casc.Configurator; +import io.jenkins.plugins.casc.ConfiguratorException; +import io.jenkins.plugins.casc.model.CNode; +import io.jenkins.plugins.casc.model.Mapping; +import org.apache.commons.lang.StringUtils; + +import java.util.Collections; +import java.util.List; + +@Extension(optional = true) +public class GitLabConfigurator extends BaseConfigurator { + + @Override + protected GitLab instance(Mapping mapping, ConfigurationContext context) throws ConfiguratorException { + if (mapping == null) { + return new GitLab("", ""); + } + final String url = (mapping.get("repoUrl") != null ? mapping.getScalarValue("repoUrl") : ""); + final String version = (mapping.get("version") != null ? mapping.getScalarValue("version") : ""); + return new GitLab(url, version); + } + + @CheckForNull + @Override + public CNode describe(GitLab instance, ConfigurationContext context) throws Exception { + Mapping mapping = new Mapping(); + mapping.put("repoUrl", StringUtils.defaultIfBlank(instance.getRepoUrl(), "")); + mapping.put("version", String.valueOf(instance.getVersion())); + return mapping; + } + + @Override + public boolean canConfigure(Class clazz) { + return clazz == GitLab.class; + } + + @Override + public Class getTarget() { + return GitLab.class; + } + + @NonNull + @Override + public List> getConfigurators(ConfigurationContext context) { + return Collections.singletonList(this); + } + +} diff --git a/src/main/java/hudson/plugins/git/extensions/GitSCMExtension.java b/src/main/java/hudson/plugins/git/extensions/GitSCMExtension.java index 20d41c15bf..64a52d28cb 100644 --- a/src/main/java/hudson/plugins/git/extensions/GitSCMExtension.java +++ b/src/main/java/hudson/plugins/git/extensions/GitSCMExtension.java @@ -1,12 +1,17 @@ package hudson.plugins.git.extensions; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; +import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractDescribableImpl; import hudson.model.AbstractProject; import hudson.model.BuildListener; +import hudson.model.Job; +import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitException; @@ -15,15 +20,15 @@ import hudson.plugins.git.util.BuildChooser; import hudson.plugins.git.util.BuildData; import hudson.scm.SCM; +import hudson.scm.SCMRevisionState; +import java.io.File; +import java.io.IOException; +import java.util.Map; import org.jenkinsci.plugins.gitclient.CheckoutCommand; import org.jenkinsci.plugins.gitclient.CloneCommand; import org.jenkinsci.plugins.gitclient.FetchCommand; -import org.jenkinsci.plugins.gitclient.MergeCommand; import org.jenkinsci.plugins.gitclient.GitClient; - -import java.io.File; -import java.io.IOException; -import java.util.Map; +import org.jenkinsci.plugins.gitclient.MergeCommand; /** * Extension point to tweak the behaviour of {@link GitSCM}. @@ -45,16 +50,22 @@ public boolean requiresWorkspaceForPolling() { * Given a commit found during polling, check whether it should be disregarded. * * - * @param scm + * @param scm GitSCM object * @param git GitClient object * @param commit * The commit whose exclusion is being tested. - * @param listener + * @param listener build log + * @param buildData build data to be used * @return * true to disregard this commit and not trigger a build, regardless of what later {@link GitSCMExtension}s say. * false to trigger a build from this commit, regardless of what later {@link GitSCMExtension}s say. * null to allow other {@link GitSCMExtension}s to decide. + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + @SuppressFBWarnings(value="NP_BOOLEAN_RETURN_NULL", justification="null used to indicate other extensions should decide") + @CheckForNull public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, TaskListener listener, BuildData buildData) throws IOException, InterruptedException, GitException { return null; } @@ -62,9 +73,28 @@ public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, Tas /** * Given the workspace root directory, gets the working directory, which is where the repository will be checked out. * + * @param scm GitSCM object + * @param context job context for workspace root + * @param workspace starting directory of workspace + * @param environment environment variables used to eval + * @param listener build log * @return working directory or null to let other {@link GitSCMExtension} control it. + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public FilePath getWorkingDirectory(GitSCM scm, Job context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException, GitException { + if (context instanceof AbstractProject) { + return getWorkingDirectory(scm, (AbstractProject) context, workspace, environment, listener); + } + return null; + } + + @Deprecated public FilePath getWorkingDirectory(GitSCM scm, AbstractProject context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "getWorkingDirectory", GitSCM.class, Job.class, FilePath.class, EnvVars.class, TaskListener.class)) { + return getWorkingDirectory(scm, (Job) context, workspace, environment, listener); + } return null; } @@ -76,7 +106,7 @@ public FilePath getWorkingDirectory(GitSCM scm, AbstractProject context, F * the chosen revision and returning it) or manipulate the state of the working tree (such as * running git-clean.) * - *

{@link #decorateRevisionToBuild(GitSCM, AbstractBuild, GitClient, BuildListener, Revision)} vs {@link BuildChooser}

+ *

{@link #decorateRevisionToBuild(GitSCM, Run, GitClient, TaskListener, Revision, Revision)} vs {@link BuildChooser}

*

* {@link BuildChooser} and this method are similar in the sense that they both participate in the process * of determining what commits to build. So when a plugin wants to control the commit to be built, you have @@ -87,43 +117,104 @@ public FilePath getWorkingDirectory(GitSCM scm, AbstractProject context, F * control what commit to build. For example the gerrit-trigger plugin looks at * a specific build parameter, then retrieves that commit from Gerrit and builds that. * - * {@link #decorateRevisionToBuild(GitSCM, AbstractBuild, GitClient, BuildListener, Revision)} is suitable + * {@link #decorateRevisionToBuild(GitSCM, Run, GitClient, TaskListener, Revision, Revision)} is suitable * when you accept arbitrary revision as an input and then create some derivative commits and then build that * result. The primary example is for speculative merge with another branch (people use this to answer * the question of "what happens if I were to integrate this feature branch back to the master branch?") * + * @param scm GitSCM object + * @param git GitClient object + * @param build run context + * @param listener build log + * @param marked + * The revision that started this build. (e.g. pre-merge) * @param rev * The revision selected for this build. * @return * The revision selected for this build. Unless you are decorating the given {@code rev}, return the value * given in the {@code rev} parameter. + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ - public Revision decorateRevisionToBuild(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, Revision rev) throws IOException, InterruptedException, GitException { - return rev; + public Revision decorateRevisionToBuild(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision marked, Revision rev) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + return decorateRevisionToBuild(scm, (AbstractBuild) build, git, (BuildListener) listener, marked, rev); + } else { + return rev; + } + } + + @Deprecated + public Revision decorateRevisionToBuild(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, Revision marked, Revision rev) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "decorateRevisionToBuild", GitSCM.class, Run.class, GitClient.class, TaskListener.class, Revision.class, Revision.class)) { + return decorateRevisionToBuild(scm, (Run) build, git, listener, marked, rev); + } else { + return rev; + } } /** * Called before the checkout activity (including fetch and checkout) starts. + * @param scm GitSCM object + * @param build run context + * @param git GitClient + * @param listener build log + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public void beforeCheckout(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + beforeCheckout(scm, (AbstractBuild) build, git, (BuildListener) listener); + } + } + + @Deprecated public void beforeCheckout(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "beforeCheckout", GitSCM.class, Run.class, GitClient.class, TaskListener.class)) { + beforeCheckout(scm, (Run) build, git, listener); + } } /** * Called when the checkout was completed and the working directory is filled with files. * - * See {@link SCM#checkout(AbstractBuild, Launcher, FilePath, BuildListener, File)} for the available parameters, + * See {@link SCM#checkout(Run, Launcher, FilePath, TaskListener, File, SCMRevisionState)} for the available parameters, * except {@code workingDirectory} * * Do not move the HEAD to another commit, as by this point the commit to be built is already determined * and recorded (such as changelog.) + * @param scm GitSCM object + * @param build run context + * @param git GitClient + * @param listener build log + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public void onCheckoutCompleted(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + onCheckoutCompleted(scm, (AbstractBuild) build, git, (BuildListener) listener); + } + } + + @Deprecated public void onCheckoutCompleted(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "onCheckoutCompleted", GitSCM.class, Run.class, GitClient.class, TaskListener.class)) { + onCheckoutCompleted(scm, (Run) build, git, listener); + } } /** * Signals when "git-clean" runs. Primarily for running "git submodule clean" * * TODO: revisit the abstraction + * @param scm GitSCM object + * @param git GitClient + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ public void onClean(GitSCM scm, GitClient git) throws IOException, InterruptedException, GitException { } @@ -131,6 +222,12 @@ public void onClean(GitSCM scm, GitClient git) throws IOException, InterruptedEx /** * Called when {@link GitClient} is created to decorate its behaviour. * This allows extensions to customize the behaviour of {@link GitClient}. + * @param scm GitSCM object + * @param git GitClient + * @return GitClient to decorate + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ public GitClient decorate(GitSCM scm, GitClient git) throws IOException, InterruptedException, GitException { return git; @@ -138,36 +235,118 @@ public GitClient decorate(GitSCM scm, GitClient git) throws IOException, Interru /** * Called before a {@link CloneCommand} is executed to allow extensions to alter its behaviour. + * @param scm GitSCM object + * @param build run context + * @param git GitClient + * @param listener build log + * @param cmd clone command to be decorated + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public void decorateCloneCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + decorateCloneCommand(scm, (AbstractBuild) build, git, (BuildListener) listener, cmd); + } + } + + @Deprecated public void decorateCloneCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "decorateCloneCommand", GitSCM.class, Run.class, GitClient.class, TaskListener.class, CloneCommand.class)) { + decorateCloneCommand(scm, (Run) build, git, listener, cmd); + } } /** * Called before a {@link FetchCommand} is executed to allow extensions to alter its behaviour. + * @param scm GitSCM object + * @param git GitClient + * @param listener build log + * @param cmd fetch command to be decorated + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error + * @deprecated use {@link #decorateCheckoutCommand(GitSCM, Run, GitClient, TaskListener, CheckoutCommand)} */ + @Deprecated public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) throws IOException, InterruptedException, GitException { } + /** + * Called before a {@link FetchCommand} is executed to allow extensions to alter its behaviour. + * @param scm GitSCM object + * @param run Run when fetch is called for Run. null during Job polling. + * @param git GitClient + * @param listener build log + * @param cmd fetch command to be decorated + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error + */ + public void decorateFetchCommand(GitSCM scm, @CheckForNull Run run, GitClient git, TaskListener listener, FetchCommand cmd) + throws IOException, InterruptedException, GitException { + decorateFetchCommand(scm, git, listener, cmd); + } + /** * Called before a {@link MergeCommand} is executed to allow extensions to alter its behaviour. + * @param scm GitSCM object + * @param build run context + * @param git GitClient + * @param listener build log + * @param cmd merge command to be decorated + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public void decorateMergeCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, MergeCommand cmd) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + decorateMergeCommand(scm, (AbstractBuild) build, git, (BuildListener) listener, cmd); + } + } + + @Deprecated public void decorateMergeCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, MergeCommand cmd) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "decorateMergeCommand", GitSCM.class, Run.class, GitClient.class, TaskListener.class, MergeCommand.class)) { + decorateMergeCommand(scm, (Run) build, git, listener, cmd); + } } /** * Called before a {@link CheckoutCommand} is executed to allow extensions to alter its behaviour. + * @param scm GitSCM object + * @param build run context + * @param git GitClient + * @param listener build log + * @param cmd checkout command to be decorated + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @throws GitException on git error */ + public void decorateCheckoutCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + if (build instanceof AbstractBuild && listener instanceof BuildListener) { + decorateCheckoutCommand(scm, (AbstractBuild) build, git, (BuildListener) listener, cmd); + } + } + + @Deprecated public void decorateCheckoutCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + if (Util.isOverridden(GitSCMExtension.class, getClass(), "decorateCheckoutCommand", GitSCM.class, Run.class, GitClient.class, TaskListener.class, CheckoutCommand.class)) { + decorateCheckoutCommand(scm, (Run) build, git, listener, cmd); + } } /** * Contribute additional environment variables for the Git invocation. + * @param scm GitSCM used as reference + * @param env environment variables to be added */ public void populateEnvironmentVariables(GitSCM scm, Map env) {} /** * Let extension declare required GitClient implementation. git-plugin will then detect conflicts, and fallback to * globally configured default git client + * @return git client type required for this extension */ public GitClientType getRequiredClient() { return GitClientType.ANY; diff --git a/src/main/java/hudson/plugins/git/extensions/GitSCMExtensionDescriptor.java b/src/main/java/hudson/plugins/git/extensions/GitSCMExtensionDescriptor.java index 9d10fb799b..c3b7cfb4a0 100644 --- a/src/main/java/hudson/plugins/git/extensions/GitSCMExtensionDescriptor.java +++ b/src/main/java/hudson/plugins/git/extensions/GitSCMExtensionDescriptor.java @@ -14,6 +14,6 @@ public boolean isApplicable(Class type) { } public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(GitSCMExtension.class); + return Jenkins.get().getDescriptorList(GitSCMExtension.class); } } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/AuthorInChangelog.java b/src/main/java/hudson/plugins/git/extensions/impl/AuthorInChangelog.java index a33a34cc83..2dc671fe8a 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/AuthorInChangelog.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/AuthorInChangelog.java @@ -17,13 +17,41 @@ public class AuthorInChangelog extends FakeGitSCMExtension { public AuthorInChangelog() { } + /** + * {@inheritDoc} + */ @Override - public boolean requiresWorkspaceForPolling() { - return true; + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return o instanceof AuthorInChangelog; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return AuthorInChangelog.class.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "AuthorInChangelog{}"; } @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Use commit author in changelog"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/BuildChooserSetting.java b/src/main/java/hudson/plugins/git/extensions/impl/BuildChooserSetting.java index dfe7a3bd5d..4898a9a7ec 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/BuildChooserSetting.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/BuildChooserSetting.java @@ -38,6 +38,9 @@ public List getBuildChooserDescriptors() { } public List getBuildChooserDescriptors(Item job) { + if (job == null) { + return getBuildChooserDescriptors(); + } return BuildChooser.allApplicableTo(job); } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/ChangelogToBranch.java b/src/main/java/hudson/plugins/git/extensions/impl/ChangelogToBranch.java new file mode 100644 index 0000000000..4d6584ed83 --- /dev/null +++ b/src/main/java/hudson/plugins/git/extensions/impl/ChangelogToBranch.java @@ -0,0 +1,40 @@ +package hudson.plugins.git.extensions.impl; + +import org.kohsuke.stapler.DataBoundConstructor; + +import hudson.Extension; +import hudson.plugins.git.ChangelogToBranchOptions; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; + +/** + * This extension activates the alternative changelog computation, + * where the changelog is calculated against a specified branch. + * + * @author Dirk Reske (dirk.reske@t-systems.com) + */ +public class ChangelogToBranch extends GitSCMExtension { + + private ChangelogToBranchOptions options; + + @DataBoundConstructor + public ChangelogToBranch(ChangelogToBranchOptions options) { + if (options == null) { + throw new IllegalArgumentException("options may not be null"); + } + this.options = options; + } + + public ChangelogToBranchOptions getOptions() { + return options; + } + + @Extension + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + + @Override + public String getDisplayName() { + return "Calculate changelog against a specific branch"; + } + } +} diff --git a/src/main/java/hudson/plugins/git/extensions/impl/CheckoutOption.java b/src/main/java/hudson/plugins/git/extensions/impl/CheckoutOption.java new file mode 100644 index 0000000000..4b312a09c9 --- /dev/null +++ b/src/main/java/hudson/plugins/git/extensions/impl/CheckoutOption.java @@ -0,0 +1,109 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.Extension; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Messages; +import hudson.plugins.git.extensions.FakeGitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; +import java.util.Objects; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Add options to the checkout command. + * + * @author Mark Waite + */ +public class CheckoutOption extends FakeGitSCMExtension { + + private Integer timeout; + + @DataBoundConstructor + public CheckoutOption(Integer timeout) { + this.timeout = timeout; + } + + public Integer getTimeout() { + return timeout; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean requiresWorkspaceForPolling() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateCheckoutCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + cmd.timeout(timeout); + } + + /** + * {@inheritDoc} + */ + @Override + @Deprecated + public void decorateCheckoutCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + cmd.timeout(timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CheckoutOption that = (CheckoutOption) o; + + return Objects.equals(timeout, that.timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "CheckoutOption{" + + "timeout=" + timeout + + '}'; + } + + @Extension + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.advanced_checkout_behaviours(); + } + } + +} diff --git a/src/main/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckout.java b/src/main/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckout.java index 4e55df94e6..4a3ad43b0b 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckout.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckout.java @@ -1,20 +1,18 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; +import java.util.Objects; -import org.jenkinsci.plugins.gitclient.CloneCommand; import org.jenkinsci.plugins.gitclient.FetchCommand; import org.jenkinsci.plugins.gitclient.GitClient; import org.kohsuke.stapler.DataBoundConstructor; - -import java.io.IOException; +import org.kohsuke.stapler.DataBoundSetter; /** * git-clean before the checkout. @@ -22,22 +20,73 @@ * @author David S Wang */ public class CleanBeforeCheckout extends GitSCMExtension { + private boolean deleteUntrackedNestedRepositories; + @DataBoundConstructor public CleanBeforeCheckout() { } + public boolean isDeleteUntrackedNestedRepositories() { + return deleteUntrackedNestedRepositories; + } + + @DataBoundSetter + public void setDeleteUntrackedNestedRepositories(boolean deleteUntrackedNestedRepositories) { + this.deleteUntrackedNestedRepositories = deleteUntrackedNestedRepositories; + } + + /** + * {@inheritDoc} + */ @Override public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) throws IOException, InterruptedException, GitException { listener.getLogger().println("Cleaning workspace"); - git.clean(); + git.clean(deleteUntrackedNestedRepositories); // TODO: revisit how to hand off to SubmoduleOption for (GitSCMExtension ext : scm.getExtensions()) { ext.onClean(scm, git); } } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CleanBeforeCheckout that = (CleanBeforeCheckout) o; + return deleteUntrackedNestedRepositories == that.deleteUntrackedNestedRepositories; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(deleteUntrackedNestedRepositories); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "CleanBeforeCheckout{" + + "deleteUntrackedNestedRepositories=" + deleteUntrackedNestedRepositories + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Clean before checkout"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/CleanCheckout.java b/src/main/java/hudson/plugins/git/extensions/impl/CleanCheckout.java index 1fee43f1ef..2e79739c0b 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/CleanCheckout.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/CleanCheckout.java @@ -1,16 +1,18 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; +import java.util.Objects; + import org.jenkinsci.plugins.gitclient.GitClient; import org.kohsuke.stapler.DataBoundConstructor; - -import java.io.IOException; +import org.kohsuke.stapler.DataBoundSetter; /** * git-clean after the checkout. @@ -18,22 +20,72 @@ * @author Kohsuke Kawaguchi */ public class CleanCheckout extends GitSCMExtension { + private boolean deleteUntrackedNestedRepositories; + @DataBoundConstructor public CleanCheckout() { } + public boolean isDeleteUntrackedNestedRepositories() { + return deleteUntrackedNestedRepositories; + } + + @DataBoundSetter + public void setDeleteUntrackedNestedRepositories(boolean deleteUntrackedNestedRepositories) { + this.deleteUntrackedNestedRepositories = deleteUntrackedNestedRepositories; + } + + /** + * {@inheritDoc} + */ @Override - public void onCheckoutCompleted(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + public void onCheckoutCompleted(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { listener.getLogger().println("Cleaning workspace"); - git.clean(); + git.clean(deleteUntrackedNestedRepositories); // TODO: revisit how to hand off to SubmoduleOption for (GitSCMExtension ext : scm.getExtensions()) { ext.onClean(scm, git); } } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CleanCheckout that = (CleanCheckout) o; + return deleteUntrackedNestedRepositories == that.deleteUntrackedNestedRepositories; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(deleteUntrackedNestedRepositories); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "CleanCheckout{" + + "deleteUntrackedNestedRepositories=" + deleteUntrackedNestedRepositories + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Clean after checkout"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/CloneOption.java b/src/main/java/hudson/plugins/git/extensions/impl/CloneOption.java index 8e43be21dc..be46f4339b 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/CloneOption.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/CloneOption.java @@ -1,74 +1,251 @@ package hudson.plugins.git.extensions.impl; +import hudson.EnvVars; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Computer; +import hudson.model.Node; +import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitClientType; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; - +import hudson.plugins.git.util.GitUtils; +import hudson.slaves.NodeProperty; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteConfig; import org.jenkinsci.plugins.gitclient.CloneCommand; import org.jenkinsci.plugins.gitclient.FetchCommand; import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; - -import java.io.IOException; +import org.kohsuke.stapler.DataBoundSetter; /** * @author Kohsuke Kawaguchi */ public class CloneOption extends GitSCMExtension { - private boolean shallow; - private String reference; - private Integer timeout; + private final boolean shallow; + private final boolean noTags; + private final String reference; + private final Integer timeout; + private Integer depth; + private boolean honorRefspec; - @DataBoundConstructor public CloneOption(boolean shallow, String reference, Integer timeout) { + this(shallow, false, reference, timeout); + } + + @DataBoundConstructor + public CloneOption(boolean shallow, boolean noTags, String reference, Integer timeout) { this.shallow = shallow; + this.noTags = noTags; this.reference = reference; this.timeout = timeout; + this.honorRefspec = false; } + @Whitelisted public boolean isShallow() { return shallow; } + @Whitelisted + public boolean isNoTags() { + return noTags; + } + + /** + * This setting allows the job definition to control whether the refspec + * will be honored during the first clone or not. + * + * Prior to git plugin 2.5.1, JENKINS-31393 caused the user provided refspec + * to be ignored during the initial clone. It was honored in later fetch + * operations, but not in the first clone. That meant the initial clone had + * to fetch all the branches and their references from the remote + * repository, even if those branches were later ignored due to the refspec. + * + * The fix for JENKINS-31393 exposed JENKINS-36507 which suggests that + * the Gerrit Plugin assumes all references are fetched, even though it only + * passes the refspec for one branch. + * + * @param honorRefspec true if refspec should be honored on clone + */ + @DataBoundSetter + public void setHonorRefspec(boolean honorRefspec) { + this.honorRefspec = honorRefspec; + } + + /** + * Returns true if the job should clone only the items which match the + * refspec, or if all references are cloned, then the refspec should be used + * in later operations. + * + * Prior to git plugin 2.5.1, JENKINS-31393 caused the user provided refspec + * to be ignored during the initial clone. It was honored in later fetch + * operations, but not in the first clone. That meant the initial clone had + * to fetch all the branches and their references from the remote + * repository, even if those branches were later ignored due to the refspec. + * + * The fix for JENKINS-31393 exposed JENKINS-36507 which seems to show that + * the Gerrit Plugin assumes all references are fetched, even though it only + * passes the refspec for one branch. + * + * @return true if initial clone will honor the user defined refspec + */ + @Whitelisted + public boolean isHonorRefspec() { + return honorRefspec; + } + + @Whitelisted public String getReference() { return reference; } - + + @Whitelisted public Integer getTimeout() { return timeout; } + @DataBoundSetter + public void setDepth(Integer depth) { + this.depth = depth; + } + + @Whitelisted + public Integer getDepth() { + return depth; + } + + /** + * {@inheritDoc} + */ @Override - public void decorateCloneCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { + public void decorateCloneCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { + cmd.shallow(shallow); if (shallow) { - listener.getLogger().println("Using shallow clone"); - cmd.shallow(); + int usedDepth = depth == null || depth < 1 ? 1 : depth; + listener.getLogger().println("Using shallow clone with depth " + usedDepth); + cmd.depth(usedDepth); + } + if (noTags) { + listener.getLogger().println("Avoid fetching tags"); + cmd.tags(false); + } + if (honorRefspec) { + listener.getLogger().println("Honoring refspec on initial clone"); + // Read refspec configuration from the first configured repository. + // Same technique is used in GitSCM. + // Assumes the passed in scm represents a single repository, or if + // multiple repositories are in use, the first repository in the + // configuration is treated as authoritative. + // Git plugin does not support multiple independent repositories + // in a single job definition. + RemoteConfig rc = scm.getRepositories().get(0); + List refspecs = rc.getFetchRefSpecs(); + cmd.refspecs(refspecs); } cmd.timeout(timeout); - cmd.reference(build.getEnvironment(listener).expand(reference)); + + Node node = GitUtils.workspaceToNode(git.getWorkTree()); + EnvVars env = build.getEnvironment(listener); + Computer comp = node.toComputer(); + if (comp != null) { + env.putAll(comp.getEnvironment()); + } + for (NodeProperty nodeProperty: node.getNodeProperties()) { + nodeProperty.buildEnvVars(env, listener); + } + cmd.reference(env.expand(reference)); } - + + /** + * {@inheritDoc} + */ @Override public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) throws IOException, InterruptedException, GitException { - cmd.timeout(timeout); + cmd.shallow(shallow); + if (shallow) { + int usedDepth = depth == null || depth < 1 ? 1 : depth; + listener.getLogger().println("Using shallow fetch with depth " + usedDepth); + cmd.depth(usedDepth); + } + cmd.tags(!noTags); + /* cmd.refspecs() not required. + * FetchCommand already requires list of refspecs through its + * from(remote, refspecs) method, no need to adjust refspecs + * here on initial clone + */ + cmd.timeout(timeout); } + /** + * {@inheritDoc} + */ @Override public GitClientType getRequiredClient() { return GitClientType.GITCLI; } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CloneOption that = (CloneOption) o; + + return shallow == that.shallow + && noTags == that.noTags + && Objects.equals(depth, that.depth) + && honorRefspec == that.honorRefspec + && Objects.equals(reference, that.reference) + && Objects.equals(timeout, that.timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(shallow, noTags, depth, honorRefspec, reference, timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "CloneOption{" + + "shallow=" + shallow + + ", noTags=" + noTags + + ", reference='" + reference + '\'' + + ", timeout=" + timeout + + ", depth=" + depth + + ", honorRefspec=" + honorRefspec + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { - return "Advanced clone behaviours"; + return Messages.Advanced_clone_behaviours(); } } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/GitLFSPull.java b/src/main/java/hudson/plugins/git/extensions/impl/GitLFSPull.java new file mode 100644 index 0000000000..94e74f4526 --- /dev/null +++ b/src/main/java/hudson/plugins/git/extensions/impl/GitLFSPull.java @@ -0,0 +1,87 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.Extension; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.transport.RemoteConfig; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * git-lfs-pull after the checkout. + * + * @author Matt Hauck + */ +public class GitLFSPull extends GitSCMExtension { + @DataBoundConstructor + public GitLFSPull() { + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateCheckoutCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + listener.getLogger().println("Enabling Git LFS pull"); + List repos = scm.getParamExpandedRepos(build, listener); + // repos should never be empty, but check anyway + if (!repos.isEmpty()) { + // Pull LFS files from the first configured repository. + // Same technique is used in GitSCM and CLoneOption. + // Assumes the passed in scm represents a single repository, or if + // multiple repositories are in use, the first repository in the + // configuration is treated as authoritative. + // Git plugin does not support multiple independent repositories + // in a single job definition. + cmd.lfsRemote(repos.get(0).getName()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return o instanceof GitLFSPull; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return GitLFSPull.class.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "GitLFSPull{}"; + } + + @Extension + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Git LFS pull after checkout"; + } + } +} diff --git a/src/main/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit.java b/src/main/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit.java index ccbfe2a087..cd5cfcb783 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit.java @@ -15,8 +15,41 @@ public class IgnoreNotifyCommit extends FakeGitSCMExtension { public IgnoreNotifyCommit() { } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return o instanceof IgnoreNotifyCommit; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return IgnoreNotifyCommit.class.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "IgnoreNotifyCommit{}"; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Don't trigger a build on commit notifications"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/LocalBranch.java b/src/main/java/hudson/plugins/git/extensions/impl/LocalBranch.java index d2ae9e9c2b..d44512f010 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/LocalBranch.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/LocalBranch.java @@ -1,33 +1,82 @@ package hudson.plugins.git.extensions.impl; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Extension; import hudson.Util; +import hudson.plugins.git.Messages; import hudson.plugins.git.extensions.FakeGitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.util.Objects; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; /** - * - * + * The Git plugin checks code out to a detached head. Configure + * LocalBranch to force checkout to a specific local branch. + * Configure this extension as null or as "**" to signify that + * the local branch name should be the same as the remote branch + * name sans the remote repository prefix (origin for example). + * * @author Kohsuke Kawaguchi */ public class LocalBranch extends FakeGitSCMExtension { - private String localBranch; + @CheckForNull + private final String localBranch; @DataBoundConstructor - public LocalBranch(String localBranch) { + public LocalBranch(@CheckForNull String localBranch) { this.localBranch = Util.fixEmptyAndTrim(localBranch); } + @CheckForNull + @Whitelisted public String getLocalBranch() { return localBranch; } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LocalBranch that = (LocalBranch) o; + + return Objects.equals(localBranch, that.localBranch); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(localBranch); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "LocalBranch{" + + (localBranch == null || "**".equals(localBranch) ? "same-as-remote" : "localBranch='"+localBranch+"'") + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { - return "Check out to specific local branch"; + return Messages.check_out_to_specific_local_branch(); } } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/git/extensions/impl/MessageExclusion.java b/src/main/java/hudson/plugins/git/extensions/impl/MessageExclusion.java new file mode 100644 index 0000000000..e5a83bc5a3 --- /dev/null +++ b/src/main/java/hudson/plugins/git/extensions/impl/MessageExclusion.java @@ -0,0 +1,76 @@ +package hudson.plugins.git.extensions.impl; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; +import hudson.model.TaskListener; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import hudson.plugins.git.util.BuildData; +import hudson.util.FormValidation; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +import java.io.IOException; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * {@link GitSCMExtension} that ignores commits with specific messages. + * + * @author Kanstantsin Shautsou + */ +public class MessageExclusion extends GitSCMExtension { + /** + * Java Pattern for matching messages to be ignored. + */ + private String excludedMessage; + + private transient volatile Pattern excludedPattern; + + @DataBoundConstructor + public MessageExclusion(String excludedMessage) { this.excludedMessage = excludedMessage; } + + @Override + public boolean requiresWorkspaceForPolling() { return true; } + + public String getExcludedMessage() { return excludedMessage; } + + @Override + @SuppressFBWarnings(value="NP_BOOLEAN_RETURN_NULL", justification="null used to indicate other extensions should decide") + @CheckForNull + public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, TaskListener listener, BuildData buildData) throws IOException, InterruptedException, GitException { + if (excludedPattern == null){ + excludedPattern = Pattern.compile(excludedMessage); + } + String msg = commit.getComment(); + if (excludedPattern.matcher(msg).matches()){ + listener.getLogger().println("Ignored commit " + commit.getId() + ": Found excluded message: " + msg); + return true; + } + + return null; + } + + @Extension + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + + public FormValidation doCheckExcludedMessage(@QueryParameter String value) { + try { + Pattern.compile(value); + } catch (PatternSyntaxException ex){ + return FormValidation.error(ex.getMessage()); + } + return FormValidation.ok(); + } + + @Override + public String getDisplayName() { + return "Polling ignores commits with certain messages"; + } + } +} diff --git a/src/main/java/hudson/plugins/git/extensions/impl/PathRestriction.java b/src/main/java/hudson/plugins/git/extensions/impl/PathRestriction.java index a358b5080a..5cb0d09d89 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/PathRestriction.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/PathRestriction.java @@ -1,5 +1,7 @@ package hudson.plugins.git.extensions.impl; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.model.TaskListener; @@ -77,7 +79,7 @@ private List getExcludedPatterns() { private List getRegionsPatterns(String[] regions) { if (regions != null) { - List patterns = new ArrayList(regions.length); + List patterns = new ArrayList<>(regions.length); for (String region : regions) { patterns.add(Pattern.compile(region)); @@ -90,6 +92,8 @@ private List getRegionsPatterns(String[] regions) { } @Override + @SuppressFBWarnings(value="NP_BOOLEAN_RETURN_NULL", justification="null used to indicate other extensions should decide") + @CheckForNull public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, TaskListener listener, BuildData buildData) { Collection paths = commit.getAffectedPaths(); if (paths.isEmpty()) {// nothing modified, so no need to compute any of this @@ -100,7 +104,7 @@ public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, Tas List excluded = getExcludedPatterns(); // Assemble the list of included paths - List includedPaths = new ArrayList(paths.size()); + List includedPaths = new ArrayList<>(paths.size()); if (!included.isEmpty()) { for (String path : paths) { for (Pattern pattern : included) { @@ -115,7 +119,7 @@ public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, Tas } // Assemble the list of excluded paths - List excludedPaths = new ArrayList(); + List excludedPaths = new ArrayList<>(); if (!excluded.isEmpty()) { for (String path : includedPaths) { for (Pattern pattern : excluded) { @@ -127,8 +131,12 @@ public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, Tas } } - // If every affected path is excluded, return true. - if (includedPaths.size() == excludedPaths.size()) { + if (excluded.isEmpty() && !included.isEmpty() && includedPaths.isEmpty()) { + listener.getLogger().println("Ignored commit " + commit.getCommitId() + + ": No paths matched included region whitelist"); + return true; + } else if (includedPaths.size() == excludedPaths.size()) { + // If every affected path is excluded, return true. listener.getLogger().println("Ignored commit " + commit.getCommitId() + ": Found only excluded paths: " + Util.join(excludedPaths, ", ")); diff --git a/src/main/java/hudson/plugins/git/extensions/impl/PerBuildTag.java b/src/main/java/hudson/plugins/git/extensions/impl/PerBuildTag.java index 90ca092370..9389ebd49c 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/PerBuildTag.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/PerBuildTag.java @@ -1,8 +1,8 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; @@ -23,9 +23,9 @@ public PerBuildTag() { } @Override - public void onCheckoutCompleted(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + public void onCheckoutCompleted(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { int buildNumber = build.getNumber(); - String buildnumber = "jenkins-" + build.getProject().getName().replace(" ", "_") + "-" + buildNumber; + String buildnumber = "jenkins-" + build.getParent().getName().replace(" ", "_") + "-" + buildNumber; git.tag(buildnumber, "Jenkins Build #" + buildNumber); } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/PreBuildMerge.java b/src/main/java/hudson/plugins/git/extensions/impl/PreBuildMerge.java index 81df102003..d0bd426941 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/PreBuildMerge.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/PreBuildMerge.java @@ -2,27 +2,32 @@ import hudson.AbortException; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.Revision; +import hudson.plugins.git.Branch; import hudson.plugins.git.UserMergeOptions; import hudson.plugins.git.extensions.GitClientType; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; import hudson.plugins.git.util.GitUtils; import hudson.plugins.git.util.MergeRecord; import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.gitclient.CheckoutCommand; import org.jenkinsci.plugins.gitclient.GitClient; import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; +import java.util.List; +import java.util.Objects; import static hudson.model.Result.FAILURE; +import hudson.model.Run; +import hudson.model.TaskListener; import static org.eclipse.jgit.lib.Constants.HEAD; /** @@ -35,7 +40,7 @@ * @author Kohsuke Kawaguchi */ public class PreBuildMerge extends GitSCMExtension { - private UserMergeOptions options; + private final UserMergeOptions options; @DataBoundConstructor public PreBuildMerge(UserMergeOptions options) { @@ -43,12 +48,13 @@ public PreBuildMerge(UserMergeOptions options) { this.options = options; } + @Whitelisted public UserMergeOptions getOptions() { return options; } @Override - public Revision decorateRevisionToBuild(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, Revision rev) throws IOException, InterruptedException { + public Revision decorateRevisionToBuild(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision marked, Revision rev) throws IOException, InterruptedException { String remoteBranchRef = GitSCM.getParameterString(options.getRef(), build.getEnvironment(listener)); // if the branch we are merging is already at the commit being built, the entire merge becomes no-op @@ -57,12 +63,12 @@ public Revision decorateRevisionToBuild(GitSCM scm, AbstractBuild build, G return rev; // Only merge if there's a branch to merge that isn't us.. - listener.getLogger().println("Merging " + rev + " onto " + remoteBranchRef + " using " + scm.getUserMergeOptions().getMergeStrategy().toString() + " strategy"); + listener.getLogger().println("Merging " + rev + " to " + remoteBranchRef + ", " + GitSCM.getParameterString(options.toString(), build.getEnvironment(listener))); // checkout origin/blah ObjectId target = git.revParse(remoteBranchRef); - String paramLocalBranch = scm.getParamLocalBranch(build); + String paramLocalBranch = scm.getParamLocalBranch(build, listener); CheckoutCommand checkoutCommand = git.checkout().branch(paramLocalBranch).ref(remoteBranchRef).deleteBranchIfExist(true); for (GitSCMExtension ext : scm.getExtensions()) ext.decorateCheckoutCommand(scm, build, git, listener, checkoutCommand); @@ -84,19 +90,46 @@ public Revision decorateRevisionToBuild(GitSCM scm, AbstractBuild build, G // record the fact that we've tried building 'rev' and it failed, or else // BuildChooser in future builds will pick up this same 'rev' again and we'll see the exact same merge failure // all over again. - scm.getBuildData(build).saveBuild(new Build(rev, build.getNumber(), FAILURE)); - throw new AbortException("Branch not suitable for integration as it does not merge cleanly"); + + // Track whether we're trying to add a duplicate BuildData, now that it's been updated with + // revision info for this build etc. The default assumption is that it's a duplicate. + BuildData buildData = scm.copyBuildData(build); + boolean buildDataAlreadyPresent = false; + List actions = build.getActions(BuildData.class); + for (BuildData d: actions) { + if (d.similarTo(buildData)) { + buildDataAlreadyPresent = true; + break; + } + } + if (!actions.isEmpty()) { + buildData.setIndex(actions.size()+1); + } + + // If the BuildData is not already attached to this build, add it to the build and mark that + // it wasn't already present, so that we add the GitTagAction and changelog after the checkout + // finishes. + if (!buildDataAlreadyPresent) { + build.addAction(buildData); + } + + buildData.saveBuild(new Build(marked,rev, build.getNumber(), FAILURE)); + throw new AbortException("Branch not suitable for integration as it does not merge cleanly: " + ex.getMessage()); } build.addAction(new MergeRecord(remoteBranchRef,target.getName())); - return new GitUtils(listener,git).getRevisionForSHA1(git.revParse(HEAD)); + Revision mergeRevision = new GitUtils(listener,git).getRevisionForSHA1(git.revParse(HEAD)); + mergeRevision.getBranches().add(new Branch(remoteBranchRef, target)); + return mergeRevision; } @Override - public void decorateMergeCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, MergeCommand cmd) throws IOException, InterruptedException, GitException { - if (scm.getUserMergeOptions().getMergeStrategy() != null) - cmd.setStrategy(scm.getUserMergeOptions().getMergeStrategy()); + public void decorateMergeCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, MergeCommand cmd) throws IOException, InterruptedException, GitException { + if (options.getMergeStrategy() != null) { + cmd.setStrategy(options.getMergeStrategy()); + } + cmd.setGitPluginFastForwardMode(options.getFastForwardMode()); } @Override @@ -104,8 +137,46 @@ public GitClientType getRequiredClient() { return GitClientType.GITCLI; } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PreBuildMerge that = (PreBuildMerge) o; + + return Objects.equals(options, that.options); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(options); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "PreBuildMerge{" + + "options=" + options.toString() + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Merge before build"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/PruneStaleBranch.java b/src/main/java/hudson/plugins/git/extensions/impl/PruneStaleBranch.java index b018b08839..63e9242a63 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/PruneStaleBranch.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/PruneStaleBranch.java @@ -1,18 +1,16 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.BuildListener; import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; import org.jenkinsci.plugins.gitclient.FetchCommand; import org.jenkinsci.plugins.gitclient.GitClient; import org.kohsuke.stapler.DataBoundConstructor; -import java.io.IOException; - /** * Prune stale remote-tracking branches * @@ -24,14 +22,50 @@ public class PruneStaleBranch extends GitSCMExtension { public PruneStaleBranch() { } + /** + * {@inheritDoc} + */ @Override public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) throws IOException, InterruptedException, GitException { listener.getLogger().println("Pruning obsolete local branches"); - cmd.prune(); + cmd.prune(true); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return o instanceof PruneStaleBranch; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return PruneStaleBranch.class.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "PruneStaleBranch{}"; } @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Prune stale remote-tracking branches"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/RelativeTargetDirectory.java b/src/main/java/hudson/plugins/git/extensions/impl/RelativeTargetDirectory.java index 2dee5e94e2..2fd024883e 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/RelativeTargetDirectory.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/RelativeTargetDirectory.java @@ -3,10 +3,11 @@ import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.model.AbstractProject; +import hudson.model.Job; import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Messages; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; import org.kohsuke.stapler.DataBoundConstructor; @@ -33,7 +34,7 @@ public String getRelativeTargetDir() { } @Override - public FilePath getWorkingDirectory(GitSCM scm, AbstractProject context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException, GitException { + public FilePath getWorkingDirectory(GitSCM scm, Job context, FilePath workspace, EnvVars environment, TaskListener listener) throws IOException, InterruptedException, GitException { if (relativeTargetDir == null || relativeTargetDir.length() == 0 || relativeTargetDir.equals(".")) { return workspace; } @@ -44,7 +45,7 @@ public FilePath getWorkingDirectory(GitSCM scm, AbstractProject context, F public static class DescriptorImpl extends GitSCMExtensionDescriptor { @Override public String getDisplayName() { - return "Check out to a sub-directory"; + return Messages.check_out_to_a_sub_directory(); } } } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/ScmName.java b/src/main/java/hudson/plugins/git/extensions/impl/ScmName.java index 445b03bfa0..cd67d9b843 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/ScmName.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/ScmName.java @@ -1,14 +1,14 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; +import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.FakeGitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; -import org.jenkinsci.plugins.multiplescms.MultiSCM; import org.kohsuke.stapler.DataBoundConstructor; /** - * When used with {@link MultiSCM}, this differentiates a different instance. - * + * When used with {@code org.jenkinsci.plugins.multiplescms.MultiSCM}, this differentiates a different instance. + * Not strictly necessary any more since {@link GitSCM#getKey} will compute a default value, but can improve visual appearance of multiple-SCM changelogs. * @author Kohsuke Kawaguchi */ public class ScmName extends FakeGitSCMExtension { diff --git a/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPath.java b/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPath.java index c5c55a6008..cbad96c4f0 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPath.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPath.java @@ -4,10 +4,12 @@ import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; -import hudson.model.Hudson; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; import java.io.Serializable; +import java.util.Objects; public class SparseCheckoutPath extends AbstractDescribableImpl implements Serializable { @@ -15,31 +17,35 @@ public class SparseCheckoutPath extends AbstractDescribableImpl getDescriptor() { - return Hudson.getInstance().getDescriptor(getClass()); + return Jenkins.get().getDescriptor(getClass()); } @Extension diff --git a/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPaths.java b/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPaths.java index 12e835e0ab..ba211c1db6 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPaths.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/SparseCheckoutPaths.java @@ -2,8 +2,8 @@ import com.google.common.collect.Lists; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; @@ -11,26 +11,29 @@ import org.jenkinsci.plugins.gitclient.CheckoutCommand; import org.jenkinsci.plugins.gitclient.CloneCommand; import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Objects; public class SparseCheckoutPaths extends GitSCMExtension { - private List sparseCheckoutPaths = Collections.emptyList(); + private List sparseCheckoutPaths; @DataBoundConstructor public SparseCheckoutPaths(List sparseCheckoutPaths) { this.sparseCheckoutPaths = sparseCheckoutPaths == null ? Collections.emptyList() : sparseCheckoutPaths; } + @Whitelisted public List getSparseCheckoutPaths() { return sparseCheckoutPaths; } @Override - public void decorateCloneCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { + public void decorateCloneCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CloneCommand cmd) throws IOException, InterruptedException, GitException { if (! sparseCheckoutPaths.isEmpty()) { listener.getLogger().println("Using no checkout clone with sparse checkout."); cmd.noCheckout(); @@ -38,7 +41,7 @@ public void decorateCloneCommand(GitSCM scm, AbstractBuild build, GitClien } @Override - public void decorateCheckoutCommand(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { + public void decorateCheckoutCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, CheckoutCommand cmd) throws IOException, InterruptedException, GitException { cmd.sparseCheckoutPaths(Lists.transform(sparseCheckoutPaths, SparseCheckoutPath.SPARSE_CHECKOUT_PATH_TO_PATH)); } @@ -49,4 +52,39 @@ public String getDisplayName() { return "Sparse Checkout paths"; } } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + SparseCheckoutPaths that = (SparseCheckoutPaths) o; + return Objects.equals(getSparseCheckoutPaths(), that.getSparseCheckoutPaths()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(getSparseCheckoutPaths()); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "SparseCheckoutPaths{" + + "sparseCheckoutPaths=" + sparseCheckoutPaths + + '}'; + } } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/SubmoduleOption.java b/src/main/java/hudson/plugins/git/extensions/impl/SubmoduleOption.java index 0aabb6bf84..292a3746c1 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/SubmoduleOption.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/SubmoduleOption.java @@ -1,18 +1,22 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Messages; import hudson.plugins.git.SubmoduleCombinator; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; import hudson.plugins.git.util.BuildData; +import java.io.IOException; +import java.util.Objects; import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.kohsuke.stapler.DataBoundConstructor; - -import java.io.IOException; +import org.kohsuke.stapler.DataBoundSetter; /** * Further tweak the behaviour of git-submodule. @@ -32,33 +36,93 @@ * @author Kohsuke Kawaguchi */ public class SubmoduleOption extends GitSCMExtension { - /** - * Use --recursive flag on submodule commands - requires git>=1.6.5 - * Use --remote flag on submodule update command - requires git>=1.8.2 - */ private boolean disableSubmodules; + /** Use --recursive flag on submodule commands - requires git>=1.6.5 */ private boolean recursiveSubmodules; + /** Use --remote flag on submodule update command - requires git>=1.8.2 */ private boolean trackingSubmodules; + /** Use --reference flag on submodule update command - requires git>=1.6.4 */ + private String reference; + private boolean parentCredentials; + private Integer timeout; + /** Use --depth flag on submodule update command - requires git>=1.8.4 */ + private boolean shallow; + private Integer depth; + private Integer threads; @DataBoundConstructor - public SubmoduleOption(boolean disableSubmodules, boolean recursiveSubmodules, boolean trackingSubmodules) { + public SubmoduleOption(boolean disableSubmodules, boolean recursiveSubmodules, boolean trackingSubmodules, String reference, Integer timeout, boolean parentCredentials) { this.disableSubmodules = disableSubmodules; this.recursiveSubmodules = recursiveSubmodules; this.trackingSubmodules = trackingSubmodules; + this.parentCredentials = parentCredentials; + this.reference = reference; + this.timeout = timeout; } + @Whitelisted public boolean isDisableSubmodules() { return disableSubmodules; } + @Whitelisted public boolean isRecursiveSubmodules() { return recursiveSubmodules; } + @Whitelisted public boolean isTrackingSubmodules() { return trackingSubmodules; } + @Whitelisted + public boolean isParentCredentials() { + return parentCredentials; + } + + @Whitelisted + public String getReference() { + return reference; + } + + @Whitelisted + public Integer getTimeout() { + return timeout; + } + + @DataBoundSetter + public void setShallow(boolean shallow) { + this.shallow = shallow; + } + + @Whitelisted + public boolean getShallow() { + return shallow; + } + + @DataBoundSetter + public void setDepth(Integer depth) { + this.depth = depth; + } + + @Whitelisted + public Integer getDepth() { + return depth; + } + + @Whitelisted + public Integer getThreads() { + return threads; + } + + @DataBoundSetter + public void setThreads(Integer threads) { + this.threads = threads; + } + + /** + * {@inheritDoc} + */ @Override public void onClean(GitSCM scm, GitClient git) throws IOException, InterruptedException, GitException { if (!disableSubmodules && git.hasGitModules()) { @@ -66,15 +130,38 @@ public void onClean(GitSCM scm, GitClient git) throws IOException, InterruptedEx } } + /** + * {@inheritDoc} + */ @Override - public void onCheckoutCompleted(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + public void onCheckoutCompleted(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { BuildData revToBuild = scm.getBuildData(build); - if (!disableSubmodules && git.hasGitModules()) { - // This ensures we don't miss changes to submodule paths and allows - // seamless use of bare and non-bare superproject repositories. - git.setupSubmoduleUrls(revToBuild.lastBuild.getRevision(), listener); - git.submoduleUpdate().recursive(recursiveSubmodules).remoteTracking(trackingSubmodules).execute(); + try { + if (!disableSubmodules && git.hasGitModules() && revToBuild != null && revToBuild.lastBuild != null) { + // This ensures we don't miss changes to submodule paths and allows + // seamless use of bare and non-bare superproject repositories. + git.setupSubmoduleUrls(revToBuild.lastBuild.getRevision(), listener); + SubmoduleUpdateCommand cmd = git.submoduleUpdate() + .recursive(recursiveSubmodules) + .remoteTracking(trackingSubmodules) + .parentCredentials(parentCredentials) + .ref(build.getEnvironment(listener).expand(reference)) + .timeout(timeout) + .shallow(shallow); + if (shallow) { + int usedDepth = depth == null || depth < 1 ? 1 : depth; + listener.getLogger().println("Using shallow submodule update with depth " + usedDepth); + cmd.depth(usedDepth); + } + int usedThreads = threads == null || threads < 1 ? 1 : threads; + cmd.threads(usedThreads); + cmd.execute(); + } + } catch (GitException e) { + // Re-throw as an IOException in order to allow generic retry + // logic to kick in properly. + throw new IOException("Could not perform submodule update", e); } if (scm.isDoGenerateSubmoduleConfigurations()) { @@ -94,11 +181,65 @@ public void onCheckoutCompleted(GitSCM scm, AbstractBuild build, GitClient } } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SubmoduleOption that = (SubmoduleOption) o; + + return disableSubmodules == that.disableSubmodules + && recursiveSubmodules == that.recursiveSubmodules + && trackingSubmodules == that.trackingSubmodules + && parentCredentials == that.parentCredentials + && Objects.equals(reference, that.reference) + && Objects.equals(timeout, that.timeout) + && shallow == that.shallow + && Objects.equals(depth, that.depth) + && Objects.equals(threads, that.threads); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(disableSubmodules, recursiveSubmodules, trackingSubmodules, parentCredentials, reference, timeout, shallow, depth, threads); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "SubmoduleOption{" + + "disableSubmodules=" + disableSubmodules + + ", recursiveSubmodules=" + recursiveSubmodules + + ", trackingSubmodules=" + trackingSubmodules + + ", reference='" + reference + '\'' + + ", parentCredentials=" + parentCredentials + + ", timeout=" + timeout + + ", shallow=" + shallow + + ", depth=" + depth + + ", threads=" + threads + + '}'; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { - return "Advanced sub-modules behaviours"; + return Messages.advanced_sub_modules_behaviours(); } } } diff --git a/src/main/java/hudson/plugins/git/extensions/impl/UserExclusion.java b/src/main/java/hudson/plugins/git/extensions/impl/UserExclusion.java index 979d7d8baf..b83533a72d 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/UserExclusion.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/UserExclusion.java @@ -1,5 +1,7 @@ package hudson.plugins.git.extensions.impl; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.model.TaskListener; import hudson.plugins.git.GitChangeSet; @@ -7,6 +9,7 @@ import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; import hudson.plugins.git.util.BuildData; +import hudson.Util; import org.jenkinsci.plugins.gitclient.GitClient; import org.kohsuke.stapler.DataBoundConstructor; @@ -14,8 +17,6 @@ import java.util.HashSet; import java.util.Set; -import static hudson.Util.*; - /** * {@link GitSCMExtension} that ignores commits that are made by specific users. * @@ -42,12 +43,12 @@ public String getExcludedUsers() { } public Set getExcludedUsersNormalized() { - String s = fixEmptyAndTrim(excludedUsers); + String s = Util.fixEmptyAndTrim(excludedUsers); if (s == null) { return Collections.emptySet(); } - Set users = new HashSet(); + Set users = new HashSet<>(); for (String user : s.split("[\\r\\n]+")) { users.add(user.trim()); } @@ -55,6 +56,8 @@ public Set getExcludedUsersNormalized() { } @Override + @SuppressFBWarnings(value="NP_BOOLEAN_RETURN_NULL", justification="null used to indicate other extensions should decide") + @CheckForNull public Boolean isRevExcluded(GitSCM scm, GitClient git, GitChangeSet commit, TaskListener listener, BuildData buildData) { String author = commit.getAuthorName(); if (getExcludedUsersNormalized().contains(author)) { diff --git a/src/main/java/hudson/plugins/git/extensions/impl/UserIdentity.java b/src/main/java/hudson/plugins/git/extensions/impl/UserIdentity.java index 16f279958c..dbfa30a42e 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/UserIdentity.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/UserIdentity.java @@ -5,13 +5,13 @@ import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; -import org.jenkinsci.plugins.gitclient.GitClient; -import org.kohsuke.stapler.DataBoundConstructor; - import java.io.IOException; import java.util.Map; +import java.util.Objects; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.kohsuke.stapler.DataBoundConstructor; -import static hudson.Util.*; +import static hudson.Util.fixEmptyAndTrim; /** * {@link GitSCMExtension} that sets a different name and/or e-mail address for commits. @@ -20,8 +20,8 @@ * @author Kohsuke Kawaguchi */ public class UserIdentity extends GitSCMExtension { - private String name; - private String email; + private final String name; + private final String email; @DataBoundConstructor public UserIdentity(String name, String email) { @@ -37,6 +37,9 @@ public String getEmail() { return email; } + /** + * {@inheritDoc} + */ @Override public void populateEnvironmentVariables(GitSCM scm, Map env) { // for backward compatibility, in case the user's shell script invokes Git inside @@ -50,6 +53,46 @@ public void populateEnvironmentVariables(GitSCM scm, Map env) { } } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UserIdentity that = (UserIdentity) o; + + return Objects.equals(name, that.name) + && Objects.equals(email, that.email); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(name, email); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "UserIdentity{" + + "name='" + name + '\'' + + ", email='" + email + '\'' + + '}'; + } + + /** + * {@inheritDoc} + */ @Override public GitClient decorate(GitSCM scm, GitClient git) throws IOException, InterruptedException, GitException { GitSCM.DescriptorImpl d = scm.getDescriptor(); @@ -68,6 +111,9 @@ public GitClient decorate(GitSCM scm, GitClient git) throws IOException, Interru @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Custom user name/e-mail address"; diff --git a/src/main/java/hudson/plugins/git/extensions/impl/WipeWorkspace.java b/src/main/java/hudson/plugins/git/extensions/impl/WipeWorkspace.java index e09b738690..3f6023d72f 100644 --- a/src/main/java/hudson/plugins/git/extensions/impl/WipeWorkspace.java +++ b/src/main/java/hudson/plugins/git/extensions/impl/WipeWorkspace.java @@ -1,17 +1,16 @@ package hudson.plugins.git.extensions.impl; import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; import org.jenkinsci.plugins.gitclient.GitClient; import org.kohsuke.stapler.DataBoundConstructor; -import java.io.IOException; - /** * Force a re-clone. * @@ -22,14 +21,50 @@ public class WipeWorkspace extends GitSCMExtension { public WipeWorkspace() { } + /** + * {@inheritDoc} + */ @Override - public void beforeCheckout(GitSCM scm, AbstractBuild build, GitClient git, BuildListener listener) throws IOException, InterruptedException, GitException { + public void beforeCheckout(GitSCM scm, Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException, GitException { listener.getLogger().println("Wiping out workspace first."); git.getWorkTree().deleteContents(); } + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return o instanceof WipeWorkspace; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return WipeWorkspace.class.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "WipeWorkspace{}"; + } + @Extension public static class DescriptorImpl extends GitSCMExtensionDescriptor { + /** + * {@inheritDoc} + */ @Override public String getDisplayName() { return "Wipe out repository & force clone"; diff --git a/src/main/java/hudson/plugins/git/opt/PreBuildMergeOptions.java b/src/main/java/hudson/plugins/git/opt/PreBuildMergeOptions.java index de72d1192a..264710ff92 100644 --- a/src/main/java/hudson/plugins/git/opt/PreBuildMergeOptions.java +++ b/src/main/java/hudson/plugins/git/opt/PreBuildMergeOptions.java @@ -31,7 +31,8 @@ public class PreBuildMergeOptions implements Serializable { */ public String mergeStrategy = MergeCommand.Strategy.DEFAULT.toString(); - @Exported + public MergeCommand.GitPluginFastForwardMode fastForwardMode = MergeCommand.GitPluginFastForwardMode.FF; + public RemoteConfig getMergeRemote() { return mergeRemote; } @@ -61,6 +62,18 @@ public void setMergeStrategy(MergeCommand.Strategy mergeStrategy) { this.mergeStrategy = mergeStrategy.toString(); } + @Exported + public MergeCommand.GitPluginFastForwardMode getFastForwardMode() { + for (MergeCommand.GitPluginFastForwardMode ffMode : MergeCommand.GitPluginFastForwardMode.values()) + if (ffMode == fastForwardMode) + return ffMode; + return MergeCommand.GitPluginFastForwardMode.FF; + } + + public void setFastForwardMode(MergeCommand.GitPluginFastForwardMode fastForwardMode) { + this.fastForwardMode = fastForwardMode; + } + @Exported public String getRemoteBranchName() { return (mergeRemote == null) ? null : mergeRemote.getName() + "/" + mergeTarget; diff --git a/src/main/java/hudson/plugins/git/util/AncestryBuildChooser.java b/src/main/java/hudson/plugins/git/util/AncestryBuildChooser.java new file mode 100644 index 0000000000..6596dd351e --- /dev/null +++ b/src/main/java/hudson/plugins/git/util/AncestryBuildChooser.java @@ -0,0 +1,158 @@ +package hudson.plugins.git.util; + +import hudson.Extension; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.Messages; +import hudson.plugins.git.Revision; +import hudson.remoting.VirtualChannel; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.jenkinsci.plugins.gitclient.GitClient; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import org.kohsuke.stapler.DataBoundConstructor; + +import com.google.common.base.Predicate; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class AncestryBuildChooser extends DefaultBuildChooser { + + private final Integer maximumAgeInDays; + private final String ancestorCommitSha1; + + @DataBoundConstructor + public AncestryBuildChooser(Integer maximumAgeInDays, String ancestorCommitSha1) { + this.maximumAgeInDays = maximumAgeInDays; + this.ancestorCommitSha1 = ancestorCommitSha1; + } + + public Integer getMaximumAgeInDays() { + return maximumAgeInDays; + } + + public String getAncestorCommitSha1() { + return ancestorCommitSha1; + } + + @Override + public Collection getCandidateRevisions(boolean isPollCall, String branchSpec, + GitClient git, final TaskListener listener, BuildData data, BuildChooserContext context) + throws GitException, IOException, InterruptedException { + + final Collection candidates = super.getCandidateRevisions(isPollCall, branchSpec, git, listener, data, context); + + // filter candidates based on branch age and ancestry + return git.withRepository((Repository repository, VirtualChannel channel) -> { + try (RevWalk walk = new RevWalk(repository)) { + + RevCommit ancestor = null; + if (!Strings.isNullOrEmpty(ancestorCommitSha1)) { + try { + ancestor = walk.parseCommit(ObjectId.fromString(ancestorCommitSha1)); + } catch (IllegalArgumentException e) { + throw new GitException(e); + } + } + + final CommitAgeFilter ageFilter = new CommitAgeFilter(maximumAgeInDays); + final AncestryFilter ancestryFilter = new AncestryFilter(walk, ancestor); + + final List filteredCandidates = Lists.newArrayList(); + + try { + for (Revision currentRevision : candidates) { + RevCommit currentRev = walk.parseCommit(ObjectId.fromString(currentRevision.getSha1String())); + + if (ageFilter.isEnabled() && !ageFilter.apply(currentRev)) { + continue; + } + + if (ancestryFilter.isEnabled() && !ancestryFilter.apply(currentRev)) { + continue; + } + + filteredCandidates.add(currentRevision); + } + } catch (Throwable e) { + + // if a wrapped IOException was thrown, unwrap before throwing it + Iterator ioeIter = Iterables.filter(Throwables.getCausalChain(e), IOException.class).iterator(); + if (ioeIter.hasNext()) + throw ioeIter.next(); + else + throw Throwables.propagate(e); + } + + return filteredCandidates; + } + }); + } + + private static class CommitAgeFilter implements Predicate { + + private LocalDateTime oldestAllowableCommitDate = null; + + public CommitAgeFilter(Integer oldestAllowableAgeInDays) { + if (oldestAllowableAgeInDays != null && oldestAllowableAgeInDays >= 0) { + this.oldestAllowableCommitDate = LocalDate.now().atStartOfDay().minusDays(oldestAllowableAgeInDays); + } + } + + @Override + public boolean apply(RevCommit rev) { + return LocalDateTime.ofInstant(rev.getCommitterIdent().getWhen().toInstant(), ZoneId.systemDefault()).isAfter(this.oldestAllowableCommitDate); + } + + public boolean isEnabled() { + return oldestAllowableCommitDate != null; + } + } + + private static class AncestryFilter implements Predicate { + + RevWalk revwalk; + RevCommit ancestor; + + public AncestryFilter(RevWalk revwalk, RevCommit ancestor) { + this.revwalk = revwalk; + this.ancestor = ancestor; + } + + public boolean apply(RevCommit rev) { + try { + return revwalk.isMergedInto(ancestor, rev); + + // wrap IOException so it can propagate + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public boolean isEnabled() { + return (revwalk != null) && (ancestor != null); + } + } + + @Extension + public static final class DescriptorImpl extends BuildChooserDescriptor { + @Override + public String getDisplayName() { + return Messages.BuildChooser_Ancestry(); + } + } + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/hudson/plugins/git/util/Build.java b/src/main/java/hudson/plugins/git/util/Build.java index edd228794c..c88bf73f0b 100644 --- a/src/main/java/hudson/plugins/git/util/Build.java +++ b/src/main/java/hudson/plugins/git/util/Build.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.io.Serializable; +import java.util.Objects; /** * Remembers which build built which {@link Revision}. @@ -91,6 +92,27 @@ public Result getBuildResult() { return "Build #" + hudsonBuildNumber + " of " + revision.toString(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Build that = (Build) o; + + return hudsonBuildNumber == that.hudsonBuildNumber + && Objects.equals(revision, that.revision) + && Objects.equals(marked, that.marked); + } + + @Override + public int hashCode() { + return Objects.hash(hudsonBuildNumber, revision, marked); + } + @Override public Build clone() { Build clone; @@ -118,4 +140,4 @@ public Object readResolve() throws IOException { marked = revision; return this; } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/git/util/BuildChooser.java b/src/main/java/hudson/plugins/git/util/BuildChooser.java index d69762fbe1..5dbda429db 100644 --- a/src/main/java/hudson/plugins/git/util/BuildChooser.java +++ b/src/main/java/hudson/plugins/git/util/BuildChooser.java @@ -1,10 +1,13 @@ package hudson.plugins.git.util; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.DescriptorExtensionList; import hudson.ExtensionPoint; import hudson.model.Describable; -import hudson.model.Hudson; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; import hudson.model.Item; import hudson.model.TaskListener; import hudson.plugins.git.GitException; @@ -13,6 +16,7 @@ import hudson.plugins.git.Revision; import org.jenkinsci.plugins.gitclient.GitClient; +import javax.annotation.ParametersAreNonnullByDefault; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; @@ -39,11 +43,13 @@ public abstract class BuildChooser implements ExtensionPoint, Describable descriptor = Jenkins.get().getDescriptor(getClass()); + return descriptor != null ? descriptor.getDisplayName() : getClass().getSimpleName(); } - + /** * Get a list of revisions that are candidates to be built. * @@ -55,22 +61,30 @@ public final String getDisplayName() { * this will be non-null only in the simple case, in advanced * cases with multiple repositories and/or branches specified * then this value will be null. + * @param git + * Used for invoking Git + * @param listener + * build log + * @param buildData build data to be used + * Information that captures what we did during the last build. * @param context * Object that provides access back to the model object. This is because - * the build chooser can be invoked on a slave where there's no direct access + * the build chooser can be invoked on an agent where there's no direct access * to the build/project for which this is invoked. * * If {@code isPollCall} is false, then call back to both project and build are available. * If {@code isPollCall} is true, then only the callback to the project is available as there's * no contextual build object. - * @return - * the candidate revision. Can be an empty set to indicate that there's nothing to build. + * @return the candidate revision. Can be an empty set to indicate that there's nothing to build. * - * @throws IOException - * @throws GitException + * @throws IOException on input or output error + * @throws GitException on git error + * @throws InterruptedException when interrupted */ - public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, - GitClient git, TaskListener listener, BuildData buildData, BuildChooserContext context) throws GitException, IOException, InterruptedException { + public Collection getCandidateRevisions(boolean isPollCall, @CheckForNull String singleBranch, + @NonNull GitClient git, @NonNull TaskListener listener, + @NonNull BuildData buildData, @NonNull BuildChooserContext context) + throws GitException, IOException, InterruptedException { // fallback to the previous signature return getCandidateRevisions(isPollCall, singleBranch, (IGitAPI) git, listener, buildData, context); } @@ -78,6 +92,30 @@ public Collection getCandidateRevisions(boolean isPollCall, String sin /** * @deprecated as of 1.2.0 * Use and override {@link #getCandidateRevisions(boolean, String, org.jenkinsci.plugins.gitclient.GitClient, hudson.model.TaskListener, BuildData, BuildChooserContext)} + * @param isPollCall true if this method is called from pollChanges. + * @param singleBranch contains the name of a single branch to be built + * this will be non-null only in the simple case, in advanced + * cases with multiple repositories and/or branches specified + * then this value will be null. + * @param git + * Used for invoking Git + * @param listener + * build log + * @param buildData + * Information that captures what we did during the last build. + * @param context + * Object that provides access back to the model object. This is because + * the build chooser can be invoked on an agent where there's no direct access + * to the build/project for which this is invoked. + * + * If {@code isPollCall} is false, then call back to both project and build are available. + * If {@code isPollCall} is true, then only the callback to the project is available as there's + * no contextual build object. + * @return + * the candidate revision. Can be an empty set to indicate that there's nothing to build. + * @throws IOException on input or output error + * @throws GitException on git error + * @throws InterruptedException when interrupted */ public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, IGitAPI git, TaskListener listener, BuildData buildData, BuildChooserContext context) throws GitException, IOException, InterruptedException { @@ -89,6 +127,19 @@ public Collection getCandidateRevisions(boolean isPollCall, String sin /** * @deprecated as of 1.1.17 * Use and override {@link #getCandidateRevisions(boolean, String, IGitAPI, TaskListener, BuildData, BuildChooserContext)} + * @param isPollCall true if this method is called from pollChanges. + * @param singleBranch contains the name of a single branch to be built + * this will be non-null only in the simple case, in advanced + * cases with multiple repositories and/or branches specified + * then this value will be null. + * @param git GitClient used to access repository + * @param listener build log + * @param buildData build data to be used + * Information that captures what we did during the last build. + * @return + * the candidate revision. Can be an empty set to indicate that there's nothing to build. + * @throws IOException on input or output error + * @throws GitException on git error */ public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, IGitAPI git, TaskListener listener, BuildData buildData) throws GitException, IOException { @@ -98,9 +149,18 @@ public Collection getCandidateRevisions(boolean isPollCall, String sin /** * @deprecated as of 1.1.25 * Use and override {@link #prevBuildForChangelog(String, BuildData, IGitAPI, BuildChooserContext)} - */ - public Build prevBuildForChangelog(String branch, @Nullable BuildData data, IGitAPI git) { - return data==null?null:data.getLastBuildOfBranch(branch); + * @param branch contains the name of branch to be built + * this will be non-null only in the simple case, in advanced + * cases with multiple repositories and/or branches specified + * then this value will be null. + * @param buildData build data to be used + * Information that captures what we did during the last build. + * @param git + * Used for invoking Git + * @return + * the candidate revision. Can be an empty set to indicate that there's nothi */ + public Build prevBuildForChangelog(String branch, @Nullable BuildData buildData, IGitAPI git) { + return buildData == null ? null : buildData.getLastBuildOfBranch(branch); } /** @@ -122,8 +182,11 @@ public Build prevBuildForChangelog(String branch, @Nullable BuildData data, IGit * Used for invoking Git * @param context * Object that provides access back to the model object. This is because - * the build chooser can be invoked on a slave where there's no direct access + * the build chooser can be invoked on an agent where there's no direct access * to the build/project for which this is invoked. + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + * @return candidate revision. Can be an empty set to indicate that there's nothing to build. */ public Build prevBuildForChangelog(String branch, @Nullable BuildData data, GitClient git, BuildChooserContext context) throws IOException,InterruptedException { return prevBuildForChangelog(branch,data, (IGitAPI) git, context); @@ -132,20 +195,46 @@ public Build prevBuildForChangelog(String branch, @Nullable BuildData data, GitC /** * @deprecated as of 1.2.0 * Use and override {@link #prevBuildForChangelog(String, BuildData, org.jenkinsci.plugins.gitclient.GitClient, BuildChooserContext)} + * @param branch contains the name of a branch to be built + * this will be non-null only in the simple case, in advanced + * cases with multiple repositories and/or branches specified + * then this value will be null. + * @param data + * Information that captures what we did during the last build. + * @param git + * Used for invoking Git + * @param context + * Object that provides access back to the model object. This is because + * the build chooser can be invoked on an agent where there's no direct access + * to the build/project for which this is invoked. + * + * If {@code isPollCall} is false, then call back to both project and build are available. + * If {@code isPollCall} is true, then only the callback to the project is available as there's + * no contextual build object. + * @return + * the candidate revision. Can be an empty set to indicate that there's nothing to build. + * @throws IOException on I/O error + * @throws GitException on git error + * @throws InterruptedException if interrupted */ public Build prevBuildForChangelog(String branch, @Nullable BuildData data, IGitAPI git, BuildChooserContext context) throws IOException,InterruptedException { return prevBuildForChangelog(branch,data,git); } + /** + * Returns build chooser descriptor. + * @return build chooser descriptor + */ public BuildChooserDescriptor getDescriptor() { - return (BuildChooserDescriptor)Hudson.getInstance().getDescriptorOrDie(getClass()); + return (BuildChooserDescriptor)Jenkins.get().getDescriptorOrDie(getClass()); } /** * All the registered build choosers. + * @return all registered build choosers */ public static DescriptorExtensionList all() { - return Hudson.getInstance() + return Jenkins.get() .getDescriptorList(BuildChooser.class); } @@ -153,9 +242,10 @@ public static DescriptorExtensionList all() * All the registered build choosers that are applicable to the specified item. * * @param item the item. + * @return All build choosers applicable to item */ public static List allApplicableTo(Item item) { - List result = new ArrayList(); + List result = new ArrayList<>(); for (BuildChooserDescriptor d: all()) { if (d.isApplicable(item.getClass())) result.add(d); @@ -164,4 +254,24 @@ public static List allApplicableTo(Item item) { } private static final long serialVersionUID = 1L; + + /** + * In a general case, a working tree is a left-over from the previous build, so it can be quite + * messed up (such as HEAD pointing to a random branch). This method is responsible to bring the + * working copy to a predictable clean state where candidate revisions can be evaluated. + *

+ * Typical use-case is a BuildChooser which do handle pull-request merge for validation. Such a + * BuildChooser will run the merge on working copy, and expose the merge commit as + * {@link BuildChooser#getCandidateRevisions(boolean, String, org.jenkinsci.plugins.gitclient.GitClient, hudson.model.TaskListener, BuildData, BuildChooserContext)} + * + * @param git client to execute git commands on working tree + * @param listener build log + * @param context back-channel to master so implementation can interact with Jenkins model + * @throws IOException on input or output error + * @throws InterruptedException when interrupted + */ + @ParametersAreNonnullByDefault + public void prepareWorkingTree(GitClient git, TaskListener listener, BuildChooserContext context) throws IOException,InterruptedException { + // Nop + } } diff --git a/src/main/java/hudson/plugins/git/util/BuildChooserContext.java b/src/main/java/hudson/plugins/git/util/BuildChooserContext.java index 5e260d17e6..7aa7bb9bb6 100644 --- a/src/main/java/hudson/plugins/git/util/BuildChooserContext.java +++ b/src/main/java/hudson/plugins/git/util/BuildChooserContext.java @@ -1,8 +1,8 @@ package hudson.plugins.git.util; import hudson.EnvVars; -import hudson.model.AbstractBuild; -import hudson.model.AbstractProject; +import hudson.model.Job; +import hudson.model.Run; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; @@ -13,16 +13,16 @@ * Provides access to the model object on the master for {@link BuildChooser}. * *

- * {@link BuildChooser} runs on a node that has the workspace, which means it can run on a slave. + * {@link BuildChooser} runs on a node that has the workspace, which means it can run on an agent. * This interface provides access for {@link BuildChooser} to send a closure to the master and execute code there. * * @author Kohsuke Kawaguchi */ public interface BuildChooserContext { - T actOnBuild(ContextCallable,T> callable) throws IOException,InterruptedException; - T actOnProject(ContextCallable,T> callable) throws IOException,InterruptedException; + T actOnBuild(ContextCallable,T> callable) throws IOException,InterruptedException; + T actOnProject(ContextCallable,T> callable) throws IOException,InterruptedException; - AbstractBuild getBuild(); + Run getBuild(); EnvVars getEnvironment(); @@ -38,6 +38,9 @@ public static interface ContextCallable extends Serializable { * @param channel * The "back pointer" of the {@link Channel} that represents the communication * with the node from where the code was sent. + * @return result from invocation on node + * @throws IOException on input or output error + * @throws InterruptedException when interrupted */ T invoke(P param, VirtualChannel channel) throws IOException, InterruptedException; } diff --git a/src/main/java/hudson/plugins/git/util/BuildChooserDescriptor.java b/src/main/java/hudson/plugins/git/util/BuildChooserDescriptor.java index d08970a73e..ad52c1e7e5 100644 --- a/src/main/java/hudson/plugins/git/util/BuildChooserDescriptor.java +++ b/src/main/java/hudson/plugins/git/util/BuildChooserDescriptor.java @@ -9,18 +9,21 @@ * @author Kohsuke Kawaguchi */ public abstract class BuildChooserDescriptor extends Descriptor { + /** - * Before this extension point is formalized, existing {@link BuildChooser}s had + * Before this extension point was formalized, existing {@link BuildChooser}s had * a hard-coded ID name used for the persistence. * * This method returns those legacy ID, if any, to keep compatibility with existing data. + * @return legacy ID, if any, to keep compatibility with existing data. */ public String getLegacyId() { return null; } public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(BuildChooser.class); + Jenkins jenkins = Jenkins.get(); + return jenkins.getDescriptorList(BuildChooser.class); } /** diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 34b2c97433..89b36980f8 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -1,6 +1,6 @@ package hudson.plugins.git.util; -import hudson.Functions; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.model.AbstractBuild; import hudson.model.Action; import hudson.model.Api; @@ -8,15 +8,27 @@ import hudson.plugins.git.Branch; import hudson.plugins.git.Revision; import hudson.plugins.git.UserRemoteConfig; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import org.eclipse.jgit.lib.ObjectId; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; -import java.io.Serializable; -import java.util.*; - import static hudson.Util.fixNull; - +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Captures the Git related information for a build. * @@ -29,13 +41,13 @@ public class BuildData implements Action, Serializable, Cloneable { private static final long serialVersionUID = 1L; /** - * Map of branch name -> build (Branch name to last built SHA1). + * Map of branch {@code name -> build} (Branch name to last built SHA1). * *

* This map contains all the branches we've built in the past (including the build that this {@link BuildData} - * is attached to) + * is attached to) */ - public Map buildsByBranchName = new HashMap(); + public Map buildsByBranchName = new HashMap<>(); /** * The last build that we did (among the values in {@link #buildsByBranchName}.) @@ -50,7 +62,13 @@ public class BuildData implements Action, Serializable, Cloneable { /** * The URLs that have been referenced. */ - public Set remoteUrls = new HashSet(); + public Set remoteUrls = new HashSet<>(); + + /** + * Allow disambiguation of the action url when multiple {@link BuildData} actions present. + */ + @CheckForNull + private Integer index; public BuildData() { } @@ -82,16 +100,45 @@ public String getDisplayName() { } public String getIconFileName() { - return Functions.getResourcePath()+"/plugin/git/icons/git-32x32.png"; + return jenkins.model.Jenkins.RESOURCE_PATH+"/plugin/git/icons/git-32x32.png"; } public String getUrlName() { - return "git"; + return index == null ? "git" : "git-"+index; + } + + /** + * Sets an identifier used to disambiguate multiple {@link BuildData} actions attached to a {@link Run} + * + * @param index the index, indexes less than or equal to {@code 1} will be discarded. + */ + public void setIndex(Integer index) { + this.index = index == null || index <= 1 ? null : index; + } + + /** + * Gets the identifier used to disambiguate multiple {@link BuildData} actions attached to a {@link Run}. + * + * @return the index. + */ + @CheckForNull + public Integer getIndex() { + return index; + } + + @Restricted(NoExternalUse.class) // only used from stapler/jelly + @CheckForNull + public Run getOwningRun() { + StaplerRequest req = Stapler.getCurrentRequest(); + if (req == null) { + return null; + } + return req.findAncestorObject(Run.class); } public Object readResolve() { - Map newBuildsByBranchName = new HashMap(); - + Map newBuildsByBranchName = new HashMap<>(); + for (Map.Entry buildByBranchName : buildsByBranchName.entrySet()) { String branchName = fixNull(buildByBranchName.getKey()); Build build = buildByBranchName.getValue(); @@ -101,29 +148,35 @@ public Object readResolve() { this.buildsByBranchName = newBuildsByBranchName; if(this.remoteUrls == null) - this.remoteUrls = new HashSet(); + this.remoteUrls = new HashSet<>(); return this; } - + /** * Return true if the history shows this SHA1 has been built. * False otherwise. - * @param sha1 + * @param sha1 SHA1 hash of commit * @return true if sha1 has been built */ public boolean hasBeenBuilt(ObjectId sha1) { - try { + return getLastBuild(sha1) != null; + } + + public Build getLastBuild(ObjectId sha1) { + // fast check by first checking most recent build + if (lastBuild != null && (lastBuild.revision.getSha1().equals(sha1) || lastBuild.marked.getSha1().equals(sha1))) return lastBuild; + try { for(Build b : buildsByBranchName.values()) { if(b.revision.getSha1().equals(sha1) || b.marked.getSha1().equals(sha1)) - return true; + return b; } - return false; - } - catch(Exception ex) { - return false; - } + return null; + } + catch(Exception ex) { + return null; + } } public void saveBuild(Build build) { @@ -131,14 +184,22 @@ public void saveBuild(Build build) { for(Branch branch : build.marked.getBranches()) { buildsByBranchName.put(fixNull(branch.getName()), build); } + for(Branch branch : build.revision.getBranches()) { + buildsByBranchName.put(fixNull(branch.getName()), build); + } } public Build getLastBuildOfBranch(String branch) { return buildsByBranchName.get(branch); } + /** + * Gets revision of the previous build. + * @return revision of the last build. + * May be null will be returned if nothing has been checked out (e.g. due to wrong repository or branch) + */ @Exported - public Revision getLastBuiltRevision() { + public @CheckForNull Revision getLastBuiltRevision() { return lastBuild==null?null:lastBuild.revision; } @@ -183,10 +244,10 @@ public BuildData clone() { throw new RuntimeException("Error cloning BuildData", e); } - IdentityHashMap clonedBuilds = new IdentityHashMap(); + IdentityHashMap clonedBuilds = new IdentityHashMap<>(); - clone.buildsByBranchName = new HashMap(); - clone.remoteUrls = new HashSet(); + clone.buildsByBranchName = new HashMap<>(); + clone.remoteUrls = new HashSet<>(); for (Map.Entry buildByBranchName : buildsByBranchName.entrySet()) { String branchName = buildByBranchName.getKey(); @@ -224,9 +285,94 @@ public Api getApi() { @Override public String toString() { - return super.toString()+"[scmName="+scmName==null?"":scmName+ + final String scmNameString = scmName == null ? "" : scmName; + return super.toString()+"[scmName="+scmNameString+ ",remoteUrls="+remoteUrls+ ",buildsByBranchName="+buildsByBranchName+ ",lastBuild="+lastBuild+"]"; } + + /** + * Returns a normalized form of a source code URL to be used in guessing if + * two different URL's are referring to the same source repository. Note + * that the comparison is only a guess. Trailing slashes are removed from + * the URL, and a trailing ".git" suffix is removed. If the input is a URL + * form (like https:// or http:// or ssh://) then URI.normalize() is called + * in an attempt to further normalize the URL. + * + * @param url repository URL to be normalized + * @return normalized URL as a string + */ + private String normalize(String url) { + if (url == null) { + return null; + } + /* Remove trailing slashes and .git suffix from URL */ + String normalized = url.replaceAll("/+$", "").replaceAll("[.]git$", ""); + if (url.contains("://")) { + /* Only URI.normalize https://, http://, and ssh://, not user@hostname:path */ + try { + /* Use URI.normalize() to further normalize the URI */ + URI uri = new URI(normalized); + normalized = uri.normalize().toString(); + } catch (URISyntaxException ex) { + LOGGER.log(Level.FINEST, "URI syntax exception on " + url, ex); + } + } + return normalized; + } + + /** + * Like {@link #equals(Object)} but doesn't check the URL as strictly, since those can vary + * while still representing the same remote repository. + * + * @param that the {@link BuildData} to compare with. + * @return {@code true} if the supplied {@link BuildData} is similar to this {@link BuildData}. + * @since 3.2.0 + */ + public boolean similarTo(BuildData that) { + if (that == null) { + return false; + } + /* Not similar if exactly one of the two remoteUrls is null */ + if ((this.remoteUrls == null) ^ (that.remoteUrls == null)) { + return false; + } + if (this.lastBuild == null ? that.lastBuild != null : !this.lastBuild.equals(that.lastBuild)) { + return false; + } + Set thisUrls = new HashSet<>(this.remoteUrls.size()); + for (String url: this.remoteUrls) { + thisUrls.add(normalize(url)); + } + Set thatUrls = new HashSet<>(that.remoteUrls.size()); + for (String url: that.remoteUrls) { + thatUrls.add(normalize(url)); + } + return thisUrls.equals(thatUrls); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BuildData that = (BuildData) o; + + return Objects.equals(remoteUrls, that.remoteUrls) + && Objects.equals(buildsByBranchName, that.buildsByBranchName) + && Objects.equals(lastBuild, that.lastBuild); + } + + @Override + public int hashCode() { + return Objects.hash(remoteUrls, buildsByBranchName, lastBuild); + } + + /* Package protected for easier testing */ + static final Logger LOGGER = Logger.getLogger(BuildData.class.getName()); } diff --git a/src/main/java/hudson/plugins/git/util/DefaultBuildChooser.java b/src/main/java/hudson/plugins/git/util/DefaultBuildChooser.java index c82ff7fdb9..6fbf33d86d 100644 --- a/src/main/java/hudson/plugins/git/util/DefaultBuildChooser.java +++ b/src/main/java/hudson/plugins/git/util/DefaultBuildChooser.java @@ -9,7 +9,6 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.RemoteConfig; import org.jenkinsci.plugins.gitclient.GitClient; -import org.jenkinsci.plugins.gitclient.RepositoryCallback; import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; @@ -34,30 +33,31 @@ public DefaultBuildChooser() { * just attempt to find the latest revision number for the chosen branch. * * If multiple branches are selected or the branches include wildcards, then - * use the advanced usecase as defined in the getAdvancedCandidateRevisons + * use the advanced usecase as defined in the getAdvancedCandidateRevisions * method. * - * @throws IOException - * @throws GitException + * @throws IOException on input or output error + * @throws GitException on git error + * @throws InterruptedException when interrupted */ @Override - public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, + public Collection getCandidateRevisions(boolean isPollCall, String branchSpec, GitClient git, TaskListener listener, BuildData data, BuildChooserContext context) throws GitException, IOException, InterruptedException { - verbose(listener,"getCandidateRevisions({0},{1},,,{2}) considering branches to build",isPollCall,singleBranch,data); + verbose(listener,"getCandidateRevisions({0},{1},,,{2}) considering branches to build",isPollCall,branchSpec,data); // if the branch name contains more wildcards then the simple usecase // does not apply and we need to skip to the advanced usecase - if (singleBranch == null || singleBranch.contains("*")) + if (isAdvancedSpec(branchSpec)) return getAdvancedCandidateRevisions(isPollCall,listener,new GitUtils(listener,git),data, context); // check if we're trying to build a specific commit // this only makes sense for a build, there is no // reason to poll for a commit - if (!isPollCall && singleBranch.matches("[0-9a-f]{6,40}")) { + if (!isPollCall && branchSpec.matches("[0-9a-f]{6,40}")) { try { - ObjectId sha1 = git.revParse(singleBranch); + ObjectId sha1 = git.revParse(branchSpec); Revision revision = new Revision(sha1); revision.getBranches().add(new Branch("detached", sha1)); verbose(listener,"Will build the detached SHA1 {0}",sha1); @@ -65,51 +65,57 @@ public Collection getCandidateRevisions(boolean isPollCall, String sin } catch (GitException e) { // revision does not exist, may still be a branch // for example a branch called "badface" would show up here - verbose(listener, "Not a valid SHA1 {0}", singleBranch); + verbose(listener, "Not a valid SHA1 {0}", branchSpec); } } - Collection revisions = new ArrayList(); + Collection revisions = new HashSet<>(); // if it doesn't contain '/' then it could be an unqualified branch - if (!singleBranch.contains("/")) { + if (!branchSpec.contains("/")) { - // BRANCH is recognized as a shorthand of */BRANCH + // BRANCH is recognized as a shorthand of */BRANCH // so check all remotes to fully qualify this branch spec for (RemoteConfig config : gitSCM.getRepositories()) { String repository = config.getName(); - String fqbn = repository + "/" + singleBranch; - verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", singleBranch, repository, fqbn); + String fqbn = repository + "/" + branchSpec; + verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn); revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data)); } } else { // either the branch is qualified (first part should match a valid remote) // or it is still unqualified, but the branch name contains a '/' - List possibleQualifiedBranches = new ArrayList(); - boolean singleBranchIsQualified = false; + List possibleQualifiedBranches = new ArrayList<>(); for (RemoteConfig config : gitSCM.getRepositories()) { String repository = config.getName(); - if (singleBranch.startsWith(repository + "/") || singleBranch.startsWith("remotes/" + repository + "/")) { - singleBranchIsQualified = true; - break; + String fqbn; + if (branchSpec.startsWith(repository + "/")) { + fqbn = "refs/remotes/" + branchSpec; + } else if(branchSpec.startsWith("remotes/" + repository + "/")) { + fqbn = "refs/" + branchSpec; + } else if(branchSpec.startsWith("refs/heads/")) { + fqbn = "refs/remotes/" + repository + "/" + branchSpec.substring("refs/heads/".length()); + } else { + //Try branchSpec as it is - e.g. "refs/tags/mytag" + fqbn = branchSpec; } - String fqbn = repository + "/" + singleBranch; - verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", singleBranch, repository, fqbn); + verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn); + possibleQualifiedBranches.add(fqbn); + + //Check if exact branch name exists + fqbn = "refs/remotes/" + repository + "/" + branchSpec; + verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn); possibleQualifiedBranches.add(fqbn); } - if (singleBranchIsQualified) { - revisions.addAll(getHeadRevision(isPollCall, singleBranch, git, listener, data)); - } else { - for (String fqbn : possibleQualifiedBranches) { - revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data)); - } + for (String fqbn : possibleQualifiedBranches) { + revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data)); } } if (revisions.isEmpty()) { // the 'branch' could actually be a non branch reference (for example a tag or a gerrit change) - revisions = getHeadRevision(isPollCall, singleBranch, git, listener, data); + revisions = getHeadRevision(isPollCall, branchSpec, git, listener, data); if (!revisions.isEmpty()) { verbose(listener, "{0} seems to be a non-branch reference (tag?)"); } @@ -187,15 +193,15 @@ private Revision objectId2Revision(String singleBranch, ObjectId sha1) { * NB: Alternate BuildChooser implementations are possible - this * may be beneficial if "only 1" branch is to be built, as much of * this work is irrelevant in that usecase. - * @throws IOException - * @throws GitException + * @throws IOException on input or output error + * @throws GitException on git error */ private List getAdvancedCandidateRevisions(boolean isPollCall, TaskListener listener, GitUtils utils, BuildData data, BuildChooserContext context) throws GitException, IOException, InterruptedException { EnvVars env = context.getEnvironment(); // 1. Get all the (branch) revisions that exist - List revs = new ArrayList(utils.getAllBranchRevisions()); + List revs = new ArrayList<>(utils.getAllBranchRevisions()); verbose(listener, "Starting with all the branches: {0}", revs); // 2. Filter out any revisions that don't contain any branches that we @@ -231,7 +237,7 @@ private List getAdvancedCandidateRevisions(boolean isPollCall, TaskLis } } - if (r.getBranches().size() == 0) { + if (r.getBranches().isEmpty()) { verbose(listener, "Ignoring {0} because we don''t care about any of the branches that point to it", r); i.remove(); } @@ -273,11 +279,9 @@ private List getAdvancedCandidateRevisions(boolean isPollCall, TaskLis // 5. sort them by the date of commit, old to new // this ensures the fairness in scheduling. final List in = revs; - return utils.git.withRepository(new RepositoryCallback>() { - public List invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { - Collections.sort(in,new CommitTimeComparator(repo)); - return in; - } + return utils.git.withRepository((Repository repo, VirtualChannel channel) -> { + Collections.sort(in,new CommitTimeComparator(repo)); + return in; }); } @@ -301,4 +305,17 @@ public String getLegacyId() { return "Default"; } } + + /** + * Helper to determine if the branchSpec requires advanced matching + * + * - if the branch name contains more wildcards then the simple usecase + * - if the branch name should be treated as regexp + * @param branchSpec branch specification + * @return true if branchSpec requires advanced matching + */ + boolean isAdvancedSpec(String branchSpec) { + // null or wildcards or regexp + return (branchSpec == null || branchSpec.contains("*") || branchSpec.startsWith(":")); + } } diff --git a/src/main/java/hudson/plugins/git/util/GitUtils.java b/src/main/java/hudson/plugins/git/util/GitUtils.java index 85355ab14f..11e53f659b 100644 --- a/src/main/java/hudson/plugins/git/util/GitUtils.java +++ b/src/main/java/hudson/plugins/git/util/GitUtils.java @@ -1,6 +1,7 @@ package hudson.plugins.git.util; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; @@ -8,16 +9,18 @@ import hudson.plugins.git.Branch; import hudson.plugins.git.BranchSpec; import hudson.plugins.git.GitException; +import hudson.plugins.git.GitObject; +import hudson.plugins.git.GitTool; import hudson.plugins.git.Revision; import hudson.remoting.VirtualChannel; import hudson.slaves.NodeProperty; +import jenkins.model.Jenkins; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.revwalk.filter.RevFilter; import org.jenkinsci.plugins.gitclient.GitClient; -import org.jenkinsci.plugins.gitclient.RepositoryCallback; import java.io.IOException; import java.io.OutputStream; @@ -26,25 +29,97 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; public class GitUtils implements Serializable { + + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="known non-serializable field") + @Nonnull GitClient git; + @Nonnull TaskListener listener; - public GitUtils(TaskListener listener, GitClient git) { + public GitUtils(@Nonnull TaskListener listener, @Nonnull GitClient git) { this.git = git; this.listener = listener; } + /** + * Resolves Git Tool by name. + * @param gitTool Tool name. If {@code null}, default tool will be used (if exists) + * @param builtOn Node for which the tool should be resolved + * Can be {@link Jenkins#getInstance()} when running on master + * @param env Additional environment variables + * @param listener Event listener + * @return Tool installation or {@code null} if it cannot be resolved + * @since TODO + */ + @CheckForNull + public static GitTool resolveGitTool(@CheckForNull String gitTool, + @CheckForNull Node builtOn, + @CheckForNull EnvVars env, + @Nonnull TaskListener listener) { + GitTool git = gitTool == null + ? GitTool.getDefaultInstallation() + : Jenkins.get().getDescriptorByType(GitTool.DescriptorImpl.class).getInstallation(gitTool); + if (git == null) { + listener.getLogger().println("Selected Git installation does not exist. Using Default"); + git = GitTool.getDefaultInstallation(); + } + if (git != null) { + if (builtOn != null) { + try { + git = git.forNode(builtOn, listener); + } catch (IOException | InterruptedException e) { + listener.getLogger().println("Failed to get git executable"); + } + } + if (env != null) { + git = git.forEnvironment(env); + } + } + return git; + } + + /** + * Resolves Git Tool by name in a node-agnostic way. + * Use {@link #resolveGitTool(String, Node, EnvVars, TaskListener)} when the node is known + * @param gitTool Tool name. If {@code null}, default tool will be used (if exists) + * @param listener Event listener + * @return Tool installation or {@code null} if it cannot be resolved + * @since TODO + */ + @CheckForNull + public static GitTool resolveGitTool(@CheckForNull String gitTool, @Nonnull TaskListener listener) { + return resolveGitTool(gitTool, null, null, listener); + } + + public static Node workspaceToNode(FilePath workspace) { // TODO https://trello.com/c/doFFMdUm/46-filepath-getcomputer + Jenkins j = Jenkins.get(); + if (workspace != null && workspace.isRemote()) { + for (Computer c : j.getComputers()) { + if (c.getChannel() == workspace.getChannel()) { + Node n = c.getNode(); + if (n != null) { + return n; + } + } + } + } + return j; + } + /** * Return a list of "Revisions" - where a revision knows about all the branch names that refer to * a SHA1. * @return list of revisions - * @throws IOException - * @throws GitException + * @throws IOException on input or output error + * @throws GitException on git error + * @throws InterruptedException when interrupted */ public Collection getAllBranchRevisions() throws GitException, IOException, InterruptedException { - Map revisions = new HashMap(); + Map revisions = new HashMap<>(); for (Branch b : git.getRemoteBranches()) { Revision r = revisions.get(b.getSHA1()); if (r == null) { @@ -53,15 +128,26 @@ public Collection getAllBranchRevisions() throws GitException, IOExcep } r.getBranches().add(b); } + for (GitObject tagEntry : git.getTags()) { + String tagRef = Constants.R_TAGS + tagEntry.getName(); + ObjectId objectId = tagEntry.getSHA1(); + Revision r = revisions.get(objectId); + if (r == null) { + r = new Revision(objectId); + revisions.put(objectId, r); + } + r.getBranches().add(new Branch(tagRef, objectId)); + } return revisions.values(); } /** * Return the revision containing the branch name. - * @param branchName + * @param branchName name of branch to be searched * @return revision containing branchName - * @throws IOException - * @throws GitException + * @throws IOException on input or output error + * @throws GitException on git error + * @throws InterruptedException when interrupted */ public Revision getRevisionContainingBranch(String branchName) throws GitException, IOException, InterruptedException { for(Revision revision : getAllBranchRevisions()) { @@ -88,8 +174,8 @@ public Revision sortBranchesForRevision(Revision revision, List bran } public Revision sortBranchesForRevision(Revision revision, List branchOrder, EnvVars env) { - ArrayList orderedBranches = new ArrayList(revision.getBranches().size()); - ArrayList revisionBranches = new ArrayList(revision.getBranches()); + ArrayList orderedBranches = new ArrayList<>(revision.getBranches().size()); + ArrayList revisionBranches = new ArrayList<>(revision.getBranches()); for(BranchSpec branchSpec : branchOrder) { for (Iterator i = revisionBranches.iterator(); i.hasNext();) { @@ -108,8 +194,9 @@ public Revision sortBranchesForRevision(Revision revision, List bran /** * Return a list of 'tip' branches (I.E. branches that aren't included entirely within another branch). * - * @param revisions + * @param revisions branches to be included in the search for tip branches * @return filtered tip branches + * @throws InterruptedException when interrupted */ @WithBridgeMethods(Collection.class) public List filterTipBranches(final Collection revisions) throws InterruptedException { @@ -118,68 +205,64 @@ public List filterTipBranches(final Collection revisions) th // \-----C // we only want (B) and (C), as (A) is an ancestor (old). - final List l = new ArrayList(revisions); + final List l = new ArrayList<>(revisions); // Bypass any rev walks if only one branch or less if (l.size() <= 1) return l; try { - return git.withRepository(new RepositoryCallback>() { - public List invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { - - // Commit nodes that we have already reached - Set visited = new HashSet(); - // Commits nodes that are tips if we don't reach them walking back from - // another node - Map tipCandidates = new HashMap(); - - long calls = 0; - final long start = System.currentTimeMillis(); - - RevWalk walk = new RevWalk(repo); - - final boolean log = LOGGER.isLoggable(Level.FINE); - - if (log) - LOGGER.fine(MessageFormat.format( - "Computing merge base of {0} branches", l.size())); - - try { - walk.setRetainBody(false); - - // Each commit passed in starts as a potential tip. - // We walk backwards in the commit's history, until we reach the - // beginning or a commit that we have already visited. In that case, - // we mark that one as not a potential tip. - for (Revision r : revisions) { - walk.reset(); - RevCommit head = walk.parseCommit(r.getSha1()); - - tipCandidates.put(head, r); - - walk.markStart(head); - for (RevCommit commit : walk) { - calls++; - if (visited.contains(commit)) { - tipCandidates.remove(commit); - break; - } - visited.add(commit); - } + return git.withRepository((Repository repo, VirtualChannel channel) -> { + // Commit nodes that we have already reached + Set visited = new HashSet<>(); + // Commits nodes that are tips if we don't reach them walking back from + // another node + Map tipCandidates = new HashMap<>(); + + long calls = 0; + final long start = System.currentTimeMillis(); + + final boolean log = LOGGER.isLoggable(Level.FINE); + + if (log) + LOGGER.fine(MessageFormat.format( + "Computing merge base of {0} branches", l.size())); + + try (RevWalk walk = new RevWalk(repo)) { + walk.setRetainBody(false); + + // Each commit passed in starts as a potential tip. + // We walk backwards in the commit's history, until we reach the + // beginning or a commit that we have already visited. In that case, + // we mark that one as not a potential tip. + for (Revision r : revisions) { + walk.reset(); + RevCommit head = walk.parseCommit(r.getSha1()); + + if (visited.contains(head)) { + continue; } - } finally { - walk.release(); + tipCandidates.put(head, r); + + walk.markStart(head); + for (RevCommit commit : walk) { + calls++; + if (visited.contains(commit)) { + tipCandidates.remove(commit); + break; + } + visited.add(commit); + } } + } - if (log) - LOGGER.fine(MessageFormat.format( - "Computed merge bases in {0} commit steps and {1} ms", calls, - (System.currentTimeMillis() - start))); + if (log) + LOGGER.fine(MessageFormat.format( + "Computed merge bases in {0} commit steps and {1} ms", calls, + (System.currentTimeMillis() - start))); - return new ArrayList(tipCandidates.values()); - } + return new ArrayList<>(tipCandidates.values()); }); } catch (IOException e) { throw new GitException("Error computing merge base", e); @@ -195,70 +278,116 @@ public static EnvVars getPollEnvironment(AbstractProject p, FilePath ws, Launche /** * An attempt to generate at least semi-useful EnvVars for polling calls, based on previous build. * Cribbed from various places. + * @param p abstract project to be considered + * @param ws workspace to be considered + * @param launcher launcher to use for calls to nodes + * @param listener build log + * @param reuseLastBuildEnv true if last build environment should be considered + * @return environment variables from previous build to be used for polling + * @throws IOException on input or output error + * @throws InterruptedException when interrupted */ public static EnvVars getPollEnvironment(AbstractProject p, FilePath ws, Launcher launcher, TaskListener listener, boolean reuseLastBuildEnv) throws IOException,InterruptedException { - EnvVars env; + EnvVars env = null; StreamBuildListener buildListener = new StreamBuildListener((OutputStream)listener.getLogger()); - AbstractBuild b = (AbstractBuild)p.getLastBuild(); + AbstractBuild b = p.getLastBuild(); + + if (b == null) { + // If there is no last build, we need to trigger a new build anyway, and + // GitSCM.compareRemoteRevisionWithImpl() will short-circuit and never call this code + // ("No previous build, so forcing an initial build."). + throw new IllegalArgumentException("Last build must not be null. If there really is no last build, " + + "a new build should be triggered without polling the SCM."); + } - if (reuseLastBuildEnv && b != null) { + if (reuseLastBuildEnv) { Node lastBuiltOn = b.getBuiltOn(); if (lastBuiltOn != null) { - env = lastBuiltOn.toComputer().getEnvironment().overrideAll(b.getCharacteristicEnvVars()); - for (NodeProperty nodeProperty: lastBuiltOn.getNodeProperties()) { - Environment environment = nodeProperty.setUp(b, launcher, (BuildListener)buildListener); - if (environment != null) { - environment.buildEnvVars(env); + Computer lastComputer = lastBuiltOn.toComputer(); + if (lastComputer != null) { + env = lastComputer.getEnvironment().overrideAll(b.getCharacteristicEnvVars()); + for (NodeProperty nodeProperty : lastBuiltOn.getNodeProperties()) { + Environment environment = nodeProperty.setUp(b, launcher, (BuildListener) buildListener); + if (environment != null) { + environment.buildEnvVars(env); + } } } - } else { - env = new EnvVars(System.getenv()); } - - p.getScm().buildEnvVars(b,env); - - if (lastBuiltOn != null) { - + if (env == null) { + env = p.getEnvironment(workspaceToNode(ws), listener); } + p.getScm().buildEnvVars(b,env); } else { - env = new EnvVars(System.getenv()); + env = p.getEnvironment(workspaceToNode(ws), listener); } - String rootUrl = Hudson.getInstance().getRootUrl(); + Jenkins jenkinsInstance = Jenkins.get(); + if (jenkinsInstance == null) { + throw new IllegalArgumentException("Jenkins instance is null"); + } + String rootUrl = jenkinsInstance.getRootUrl(); if(rootUrl!=null) { env.put("HUDSON_URL", rootUrl); // Legacy. env.put("JENKINS_URL", rootUrl); - if( b != null) env.put("BUILD_URL", rootUrl+b.getUrl()); + env.put("BUILD_URL", rootUrl+b.getUrl()); env.put("JOB_URL", rootUrl+p.getUrl()); } if(!env.containsKey("HUDSON_HOME")) // Legacy - env.put("HUDSON_HOME", Hudson.getInstance().getRootDir().getPath() ); + env.put("HUDSON_HOME", jenkinsInstance.getRootDir().getPath() ); if(!env.containsKey("JENKINS_HOME")) - env.put("JENKINS_HOME", Hudson.getInstance().getRootDir().getPath() ); + env.put("JENKINS_HOME", jenkinsInstance.getRootDir().getPath() ); if (ws != null) env.put("WORKSPACE", ws.getRemote()); - for (NodeProperty nodeProperty: Hudson.getInstance().getGlobalNodeProperties()) { + for (NodeProperty nodeProperty: jenkinsInstance.getGlobalNodeProperties()) { Environment environment = nodeProperty.setUp(b, launcher, (BuildListener)buildListener); if (environment != null) { environment.buildEnvVars(env); } } + // add env contributing actions' values from last build to environment - fixes JENKINS-22009 + addEnvironmentContributingActionsValues(env, b); + EnvVars.resolve(env); return env; } + private static void addEnvironmentContributingActionsValues(EnvVars env, AbstractBuild b) { + List buildActions = b.getAllActions(); + if (buildActions != null) { + for (Action action : buildActions) { + // most importantly, ParametersAction will be processed here (for parameterized builds) + if (action instanceof ParametersAction) { + ParametersAction envAction = (ParametersAction) action; + envAction.buildEnvVars(b, env); + } + } + } + + // Use the default parameter values (if any) instead of the ones from the last build + ParametersDefinitionProperty paramDefProp = (ParametersDefinitionProperty) b.getProject().getProperty(ParametersDefinitionProperty.class); + if (paramDefProp != null) { + for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) { + ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); + if (defaultValue != null) { + defaultValue.buildEnvironment(b, env); + } + } + } + } + public static String[] fixupNames(String[] names, String[] urls) { String[] returnNames = new String[urls.length]; - Set usedNames = new HashSet(); + Set usedNames = new HashSet<>(); for(int i=0; iexcept for those which match the + * Git build chooser which will select all branches except for those which match the * configured branch specifiers. *

- * e.g. If **/master and **/release-* are configured as - * "Branches to build" then any branches matching those patterns will not be built, unless + * e.g. If {@code **/master} and {@code **/release-*} are configured as + * "Branches to build" then any branches matching those patterns will not be built, unless * another branch points to the same revision. *

- * This is useful, for example, when you have jobs building your master and various - * release branches and you want a second job which builds all new feature branches — + * This is useful, for example, when you have jobs building your {@code master} and various + * {@code release} branches and you want a second job which builds all new feature branches — * i.e. branches which do not match these patterns — without redundantly building - * master and the release branches again each time they change. + * {@code master} and the release branches again each time they change. * * @author Christopher Orr */ @@ -44,7 +43,7 @@ public Collection getCandidateRevisions(boolean isPollCall, EnvVars env = context.getEnvironment(); GitUtils utils = new GitUtils(listener, git); - List branchRevs = new ArrayList(utils.getAllBranchRevisions()); + List branchRevs = new ArrayList<>(utils.getAllBranchRevisions()); List specifiedBranches = gitSCM.getBranches(); // Iterate over all the revisions pointed to by branches in the repository @@ -95,11 +94,9 @@ public Collection getCandidateRevisions(boolean isPollCall, // Sort revisions by the date of commit, old to new, to ensure fairness in scheduling final List in = branchRevs; - return utils.git.withRepository(new RepositoryCallback>() { - public List invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { - Collections.sort(in,new CommitTimeComparator(repo)); - return in; - } + return utils.git.withRepository((Repository repo, VirtualChannel channel) -> { + Collections.sort(in,new CommitTimeComparator(repo)); + return in; }); } diff --git a/src/main/java/hudson/plugins/git/util/RevCommitRepositoryCallback.java b/src/main/java/hudson/plugins/git/util/RevCommitRepositoryCallback.java new file mode 100644 index 0000000000..cdc530bcdf --- /dev/null +++ b/src/main/java/hudson/plugins/git/util/RevCommitRepositoryCallback.java @@ -0,0 +1,28 @@ +package hudson.plugins.git.util; + +import hudson.remoting.VirtualChannel; +import java.io.IOException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.jenkinsci.plugins.gitclient.RepositoryCallback; + +/** + * Retrieves {@link RevCommit} from given {@link Build} revision. + */ +public final class RevCommitRepositoryCallback implements RepositoryCallback { + private static final long serialVersionUID = 1L; + private final Build revToBuild; + + public RevCommitRepositoryCallback(Build revToBuild) { + this.revToBuild = revToBuild; + } + + @Override + public RevCommit invoke(Repository repository, VirtualChannel virtualChannel) + throws IOException, InterruptedException { + try (RevWalk walk = new RevWalk(repository)) { + return walk.parseCommit(revToBuild.revision.getSha1()); + } + } +} \ No newline at end of file diff --git a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java index 6f122ee060..465d3c82e5 100644 --- a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java +++ b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2013, CloudBees, Inc., Stephen Connolly. + * Copyright (c) 2013-2017, CloudBees, Inc., Stephen Connolly, Amadeus IT Group. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,95 +24,317 @@ package jenkins.plugins.git; import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; -import hudson.Extension; +import hudson.RestrictedSince; import hudson.Util; +import hudson.model.Action; +import hudson.model.Actionable; import hudson.model.Item; import hudson.model.TaskListener; import hudson.plugins.git.Branch; -import hudson.plugins.git.BranchSpec; import hudson.plugins.git.GitException; import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTool; import hudson.plugins.git.Revision; -import hudson.plugins.git.SubmoduleConfig; import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.GitRepositoryBrowser; import hudson.plugins.git.extensions.GitSCMExtension; -import hudson.plugins.git.extensions.impl.BuildChooserSetting; import hudson.plugins.git.util.Build; import hudson.plugins.git.util.BuildChooser; import hudson.plugins.git.util.BuildChooserContext; -import hudson.plugins.git.util.BuildChooserDescriptor; import hudson.plugins.git.util.BuildData; -import hudson.plugins.git.util.DefaultBuildChooser; +import hudson.plugins.git.util.GitUtils; import hudson.scm.SCM; import hudson.security.ACL; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; import jenkins.model.Jenkins; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; +import jenkins.plugins.git.traits.GitSCMExtensionTrait; +import jenkins.plugins.git.traits.GitToolSCMSourceTrait; +import jenkins.plugins.git.traits.RefSpecsSCMSourceTrait; +import jenkins.plugins.git.traits.RemoteNameSCMSourceTrait; +import jenkins.scm.api.SCMFile; +import jenkins.scm.api.SCMFileSystem; import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMProbe; +import jenkins.scm.api.SCMProbeStat; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.SCMSourceEvent; import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMTrait; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import jenkins.scm.impl.trait.WildcardSCMSourceFilterTrait; +import net.jcip.annotations.GuardedBy; import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RefSpec; -import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.treewalk.TreeWalk; +import org.jenkinsci.plugins.gitclient.FetchCommand; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; - -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.export.Exported; /** - * @author Stephen Connolly + * Base class for {@link SCMSource} implementations that produce {@link GitSCM} implementations. + * + * @since 2.0 */ public abstract class AbstractGitSCMSource extends SCMSource { + /** + * The default remote name to use when configuring the ref specs to use with fetch operations. + * + * @since 3.4.0 + */ + public static final String DEFAULT_REMOTE_NAME = "origin"; + /** + * The placeholder to use in ref spec templates in order to correctly ensure that the ref spec remote name + * matches the remote name. + *

+ * The template uses {@code @{...}} as that is an illegal sequence in a remote name + * + * @see git + * source code rules on ref spec names + * @since 3.4.0 + */ + public static final String REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR = "@{remote}"; + /** + * The regex for {@link #REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR}. + * + * @since 3.4.0 + */ + public static final String REF_SPEC_REMOTE_NAME_PLACEHOLDER = "(?i)"+Pattern.quote(REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR); + /** + * The default ref spec template. + * + * @since 3.4.0 + */ + public static final String REF_SPEC_DEFAULT = + "+refs/heads/*:refs/remotes/" + REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR + "/*"; + /** * Keep one lock per cache directory. Lazy populated, but never purge, except on restart. */ - private static final ConcurrentMap cacheLocks = new ConcurrentHashMap(); + private static final ConcurrentMap cacheLocks = new ConcurrentHashMap<>(); + + private static final Logger LOGGER = Logger.getLogger(AbstractGitSCMSource.class.getName()); + public AbstractGitSCMSource() { + } + + @Deprecated public AbstractGitSCMSource(String id) { - super(id); + setId(id); } + @CheckForNull public abstract String getCredentialsId(); + /** + * @return Git remote URL + */ public abstract String getRemote(); - public abstract String getIncludes(); + /** + * @deprecated use {@link WildcardSCMSourceFilterTrait} + * @return the includes. + */ + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public String getIncludes() { + WildcardSCMHeadFilterTrait trait = SCMTrait.find(getTraits(), WildcardSCMHeadFilterTrait.class); + return trait != null ? trait.getIncludes() : "*"; + } - public abstract String getExcludes(); + /** + * @return the excludes. + * @deprecated use {@link WildcardSCMSourceFilterTrait} + */ + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public String getExcludes() { + WildcardSCMHeadFilterTrait trait = SCMTrait.find(getTraits(), WildcardSCMHeadFilterTrait.class); + return trait != null ? trait.getExcludes() : ""; + } + /** + * Gets {@link GitRepositoryBrowser} to be used with this SCMSource. + * @return Repository browser or {@code null} if the default tool should be used. + * @since 2.5.1 + * @deprecated use {@link GitBrowserSCMSourceTrait} + */ + @CheckForNull + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public GitRepositoryBrowser getBrowser() { + GitBrowserSCMSourceTrait trait = SCMTrait.find(getTraits(), GitBrowserSCMSourceTrait.class); + return trait != null ? trait.getBrowser() : null; + } + + /** + * Gets Git tool to be used for this SCM Source. + * @return Git Tool or {@code null} if the default tool should be used. + * @since 2.5.1 + * @deprecated use {@link GitToolSCMSourceTrait} + */ + @CheckForNull + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public String getGitTool() { + GitToolSCMSourceTrait trait = SCMTrait.find(getTraits(), GitToolSCMSourceTrait.class); + return trait != null ? trait.getGitTool() : null; + } + + /** + * Gets list of extensions, which should be used with this branch source. + * @return List of Extensions to be used. May be empty + * @since 2.5.1 + * @deprecated use corresponding {@link GitSCMExtensionTrait} (and if there isn't one then likely the + * {@link GitSCMExtension} is not appropriate to use in the context of a {@link SCMSource}) + */ + @NonNull + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public List getExtensions() { + List extensions = new ArrayList<>(); + for (SCMSourceTrait t : getTraits()) { + if (t instanceof GitSCMExtensionTrait) { + extensions.add(((GitSCMExtensionTrait) t).getExtension()); + } + } + return Collections.unmodifiableList(extensions); + } + + /** + * Returns the {@link SCMSourceTrait} instances for this {@link AbstractGitSCMSource}. + * @return the {@link SCMSourceTrait} instances + * @since 3.4.0 + */ + @NonNull + public List getTraits() { + // Always return empty list (we expect subclasses to override) + return Collections.emptyList(); + } + + /** + * @deprecated use {@link RemoteNameSCMSourceTrait} + * @return the remote name. + */ + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") public String getRemoteName() { - return "origin"; + RemoteNameSCMSourceTrait trait = SCMTrait.find(getTraits(), RemoteNameSCMSourceTrait.class); + return trait != null ? trait.getRemoteName() : DEFAULT_REMOTE_NAME; } + /** + * Resolves the {@link GitTool}. + * @return the {@link GitTool}. + * @deprecated use {@link #resolveGitTool(String)}. + */ @CheckForNull - @Override - protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener listener) + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + protected GitTool resolveGitTool() { + return resolveGitTool(getGitTool()); + } + + /** + * Resolves the {@link GitTool}. + * @param gitTool the {@link GitTool#getName()} to resolve. + * @return the {@link GitTool} + * @since 3.4.0 + * @deprecated Use {@link #resolveGitTool(String, TaskListener)} instead + */ + @CheckForNull + @Deprecated + protected GitTool resolveGitTool(String gitTool) { + return resolveGitTool(gitTool, TaskListener.NULL); + } + + protected GitTool resolveGitTool(String gitTool, TaskListener listener) { + final Jenkins jenkins = Jenkins.get(); + return GitUtils.resolveGitTool(gitTool, jenkins, null, TaskListener.NULL); + } + + private interface Retriever { + default T run(GitClient client, String remoteName) throws IOException, InterruptedException { + throw new AbstractMethodError("Not implemented"); + } + } + + private interface Retriever2 extends Retriever { + T run(GitClient client, String remoteName, FetchCommand fetch) throws IOException, InterruptedException; + } + + @NonNull + private , R extends GitSCMSourceRequest> T doRetrieve(Retriever retriever, + @NonNull C context, + @NonNull TaskListener listener, + boolean prune) + throws IOException, InterruptedException { + return doRetrieve(retriever, context, listener, prune, false); + } + + @NonNull + private , R extends GitSCMSourceRequest> T doRetrieve(Retriever retriever, + @NonNull C context, + @NonNull TaskListener listener, + boolean prune, boolean delayFetch) throws IOException, InterruptedException { String cacheEntry = getCacheEntry(); Lock cacheLock = getCacheLock(cacheEntry); @@ -120,132 +342,887 @@ protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener list try { File cacheDir = getCacheDir(cacheEntry); Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)).in(cacheDir); + GitTool tool = resolveGitTool(context.gitTool(), listener); + if (tool != null) { + git.using(tool.getGitExe()); + } GitClient client = git.getClient(); client.addDefaultCredentials(getCredentials()); if (!client.hasGitRepo()) { listener.getLogger().println("Creating git repository in " + cacheDir); client.init(); } - String remoteName = getRemoteName(); + String remoteName = context.remoteName(); listener.getLogger().println("Setting " + remoteName + " to " + getRemote()); client.setRemoteUrl(remoteName, getRemote()); - listener.getLogger().println("Fetching " + remoteName + "..."); - List refSpecs = getRefSpecs(); - client.fetch(remoteName, refSpecs.toArray(new RefSpec[refSpecs.size()])); - // we don't prune remotes here, as we just want one head's revision - for (Branch b : client.getRemoteBranches()) { - String branchName = StringUtils.removeStart(b.getName(), remoteName + "/"); - if (branchName.equals(head.getName())) { - return new SCMRevisionImpl(head, b.getSHA1String()); - } + listener.getLogger().println((prune ? "Fetching & pruning " : "Fetching ") + remoteName + "..."); + FetchCommand fetch = client.fetch_(); + fetch = fetch.prune(prune); + + URIish remoteURI = null; + try { + remoteURI = new URIish(remoteName); + } catch (URISyntaxException ex) { + listener.getLogger().println("URI syntax exception for '" + remoteName + "' " + ex); } - return null; + final FetchCommand fetchCommand = fetch.from(remoteURI, context.asRefSpecs()); + if (!delayFetch) { + fetchCommand.execute(); + } else if (retriever instanceof Retriever2) { + return ((Retriever2)retriever).run(client, remoteName, fetchCommand); + } + return retriever.run(client, remoteName); } finally { cacheLock.unlock(); } } - @NonNull + /** + * {@inheritDoc} + */ + @CheckForNull @Override - protected void retrieve(@NonNull final SCMHeadObserver observer, - @NonNull TaskListener listener) + protected SCMRevision retrieve(@NonNull final SCMHead head, @NonNull final TaskListener listener) throws IOException, InterruptedException { - String cacheEntry = getCacheEntry(); - Lock cacheLock = getCacheLock(cacheEntry); - cacheLock.lock(); - try { - File cacheDir = getCacheDir(cacheEntry); - Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)).in(cacheDir); - GitClient client = git.getClient(); - client.addDefaultCredentials(getCredentials()); - if (!client.hasGitRepo()) { - listener.getLogger().println("Creating git repository in " + cacheDir); - client.init(); + GitSCMSourceContext context = new GitSCMSourceContext<>(null, SCMHeadObserver.none()).withTraits(getTraits()); + GitSCMTelescope telescope = GitSCMTelescope.of(this); + if (telescope != null) { + String remote = getRemote(); + StandardUsernameCredentials credentials = getCredentials(); + telescope.validate(remote, credentials); + return telescope.getRevision(remote, credentials, head); + } + //TODO write test using GitRefSCMHead + return doRetrieve(new Retriever() { + @Override + public SCMRevision run(GitClient client, String remoteName) throws IOException, InterruptedException { + if (head instanceof GitTagSCMHead) { + try { + ObjectId objectId = client.revParse(Constants.R_TAGS + head.getName()); + return new GitTagSCMRevision((GitTagSCMHead) head, objectId.name()); + } catch (GitException e) { + // tag does not exist + return null; + } + } else if (head instanceof GitBranchSCMHead) { + for (Branch b : client.getRemoteBranches()) { + String branchName = StringUtils.removeStart(b.getName(), remoteName + "/"); + if (branchName.equals(head.getName())) { + return new GitBranchSCMRevision((GitBranchSCMHead)head, b.getSHA1String()); + } + } + } else if (head instanceof GitRefSCMHead) { + try { + ObjectId objectId = client.revParse(((GitRefSCMHead) head).getRef()); + return new GitRefSCMRevision((GitRefSCMHead)head, objectId.name()); + } catch (GitException e) { + // ref could not be found + return null; + } + } else { + //Entering default/legacy git retrieve code path + for (Branch b : client.getRemoteBranches()) { + String branchName = StringUtils.removeStart(b.getName(), remoteName + "/"); + if (branchName.equals(head.getName())) { + return new SCMRevisionImpl(head, b.getSHA1String()); + } + } + } + return null; + } + }, + context, + listener, /* we don't prune remotes here, as we just want one head's revision */false); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="Known non-serializable this") + protected void retrieve(@CheckForNull SCMSourceCriteria criteria, + @NonNull SCMHeadObserver observer, + @CheckForNull SCMHeadEvent event, + @NonNull final TaskListener listener) + throws IOException, InterruptedException { + final GitSCMSourceContext context = + new GitSCMSourceContext<>(criteria, observer).withTraits(getTraits()); + final GitSCMTelescope telescope = GitSCMTelescope.of(this); + if (telescope != null) { + final String remote = getRemote(); + final StandardUsernameCredentials credentials = getCredentials(); + telescope.validate(remote, credentials); + Set referenceTypes = new HashSet<>(); + if (context.wantBranches()) { + referenceTypes.add(GitSCMTelescope.ReferenceType.HEAD); } - String remoteName = getRemoteName(); - listener.getLogger().println("Setting " + remoteName + " to " + getRemote()); - client.setRemoteUrl(remoteName, getRemote()); - listener.getLogger().println("Fetching " + remoteName + "..."); - List refSpecs = getRefSpecs(); - client.fetch(remoteName, refSpecs.toArray(new RefSpec[refSpecs.size()])); - listener.getLogger().println("Pruning stale remotes..."); - final Repository repository = client.getRepository(); - try { - client.prune(new RemoteConfig(repository.getConfig(), remoteName)); - } catch (UnsupportedOperationException e) { - e.printStackTrace(listener.error("Could not prune stale remotes")); - } catch (URISyntaxException e) { - e.printStackTrace(listener.error("Could not prune stale remotes")); - } - listener.getLogger().println("Getting remote branches..."); - SCMSourceCriteria branchCriteria = getCriteria(); - RevWalk walk = new RevWalk(repository); - try { - walk.setRetainBody(false); - for (Branch b : client.getRemoteBranches()) { - if (!b.getName().startsWith(remoteName + "/")) { - continue; + if (context.wantTags()) { + referenceTypes.add(GitSCMTelescope.ReferenceType.TAG); + } + //TODO JENKINS-51134 DiscoverOtherRefsTrait + if (!referenceTypes.isEmpty()) { + try (GitSCMSourceRequest request = context.newRequest(AbstractGitSCMSource.this, listener)) { + listener.getLogger().println("Listing remote references..."); + Iterable revisions = telescope.getRevisions(remote, credentials, referenceTypes); + if (context.wantBranches()) { + listener.getLogger().println("Checking branches..."); + int count = 0; + for (final SCMRevision revision : revisions) { + if (!(revision instanceof SCMRevisionImpl) || (revision instanceof GitTagSCMRevision)) { + continue; + } + count++; + if (request.process(revision.getHead(), + new SCMSourceRequest.RevisionLambda() { + @NonNull + @Override + public SCMRevisionImpl create(@NonNull SCMHead head) + throws IOException, InterruptedException { + listener.getLogger() + .println(" Checking branch " + revision.getHead().getName()); + return (SCMRevisionImpl) revision; + } + }, + new SCMSourceRequest.ProbeLambda() { + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull SCMHead head, + @NonNull SCMRevisionImpl revision) + throws IOException, InterruptedException { + return new TelescopingSCMProbe(telescope, remote, credentials, revision); + } + }, new SCMSourceRequest.Witness() { + @Override + public void record(@NonNull SCMHead head, SCMRevision revision, + boolean isMatch) { + if (isMatch) { + listener.getLogger().println(" Met criteria"); + } else { + listener.getLogger().println(" Does not meet criteria"); + } + } + } + )) { + listener.getLogger().format("Processed %d branches (query complete)%n", count); + return; + } + } + listener.getLogger().format("Processed %d branches%n", count); } - final String branchName = StringUtils.removeStart(b.getName(), remoteName + "/"); - listener.getLogger().println("Checking branch " + branchName); - if (branchCriteria != null) { - RevCommit commit = walk.parseCommit(b.getSHA1()); - final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); - final RevTree tree = commit.getTree(); - SCMSourceCriteria.Probe probe = new SCMSourceCriteria.Probe() { - @Override - public String name() { - return branchName; + if (context.wantTags()) { + listener.getLogger().println("Checking tags..."); + int count = 0; + for (final SCMRevision revision : revisions) { + if (!(revision instanceof GitTagSCMRevision)) { + continue; + } + count++; + if (request.process((GitTagSCMHead) revision.getHead(), + new SCMSourceRequest.RevisionLambda() { + @NonNull + @Override + public GitTagSCMRevision create(@NonNull GitTagSCMHead head) + throws IOException, InterruptedException { + listener.getLogger() + .println(" Checking tag " + revision.getHead().getName()); + return (GitTagSCMRevision) revision; + } + }, + new SCMSourceRequest.ProbeLambda() { + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull final GitTagSCMHead head, + @NonNull GitTagSCMRevision revision) + throws IOException, InterruptedException { + return new TelescopingSCMProbe(telescope, remote, credentials, revision); + } + }, new SCMSourceRequest.Witness() { + @Override + public void record(@NonNull SCMHead head, SCMRevision revision, + boolean isMatch) { + if (isMatch) { + listener.getLogger().println(" Met criteria"); + } else { + listener.getLogger().println(" Does not meet criteria"); + } + } + } + )) { + listener.getLogger().format("Processed %d tags (query complete)%n", count); + return; } + } + listener.getLogger().format("Processed %d tags%n", count); + } + } + return; + } + } + doRetrieve(new Retriever2() { + @Override + public Void run(GitClient client, String remoteName, FetchCommand fetch) throws IOException, InterruptedException { + final Map remoteReferences; + if (context.wantBranches() || context.wantTags() || context.wantOtherRefs()) { + listener.getLogger().println("Listing remote references..."); + boolean headsOnly = !context.wantOtherRefs() && context.wantBranches(); + boolean tagsOnly = !context.wantOtherRefs() && context.wantTags(); + remoteReferences = client.getRemoteReferences( + client.getRemoteUrl(remoteName), null, headsOnly, tagsOnly + ); + } else { + remoteReferences = Collections.emptyMap(); + } + fetch.execute(); + try (Repository repository = client.getRepository(); + RevWalk walk = new RevWalk(repository); + GitSCMSourceRequest request = context.newRequest(AbstractGitSCMSource.this, listener)) { + + if (context.wantBranches()) { + discoverBranches(repository, walk, request, remoteReferences); + } + if (context.wantTags()) { + discoverTags(repository, walk, request, remoteReferences); + } + if (context.wantOtherRefs()) { + discoverOtherRefs(repository, walk, request, remoteReferences, + (Collection)context.getRefNameMappings()); + } + } + return null; + } - @Override - public long lastModified() { - return lastModified; + private void discoverOtherRefs(final Repository repository, + final RevWalk walk, GitSCMSourceRequest request, + Map remoteReferences, + Collection wantedRefs) + throws IOException, InterruptedException { + listener.getLogger().println("Checking other refs..."); + walk.setRetainBody(false); + int count = 0; + for (final Map.Entry ref : remoteReferences.entrySet()) { + if (ref.getKey().startsWith(Constants.R_HEADS) || ref.getKey().startsWith(Constants.R_TAGS)) { + continue; + } + for (GitSCMSourceContext.RefNameMapping otherRef : wantedRefs) { + if (!otherRef.matches(ref.getKey())) { + continue; + } + final String refName = otherRef.getName(ref.getKey()); + if (refName == null) { + listener.getLogger().println(" Possible badly configured name mapping (" + otherRef.getName() + ") (for " + ref.getKey() + ") ignoring."); + continue; + } + count++; + if (request.process(new GitRefSCMHead(refName, ref.getKey()), + new SCMSourceRequest.IntermediateLambda() { + @Nullable + @Override + public ObjectId create() throws IOException, InterruptedException { + listener.getLogger().println(" Checking ref " + refName + " (" + ref.getKey() + ")"); + return ref.getValue(); + } + }, + new SCMSourceRequest.ProbeLambda() { + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull GitRefSCMHead head, + @Nullable ObjectId revisionInfo) + throws IOException, InterruptedException { + RevCommit commit = walk.parseCommit(revisionInfo); + final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + final RevTree tree = commit.getTree(); + return new TreeWalkingSCMProbe(refName, lastModified, repository, tree); + } + }, new SCMSourceRequest.LazyRevisionLambda() { + @NonNull + @Override + public SCMRevision create(@NonNull GitRefSCMHead head, @Nullable ObjectId intermediate) + throws IOException, InterruptedException { + return new GitRefSCMRevision(head, ref.getValue().name()); + } + }, new SCMSourceRequest.Witness() { + @Override + public void record(@NonNull SCMHead head, SCMRevision revision, boolean isMatch) { + if (isMatch) { + listener.getLogger().println(" Met criteria"); + } else { + listener.getLogger().println(" Does not meet criteria"); + } + } + } + )) { + listener.getLogger().format("Processed %d refs (query complete)%n", count); + return; + } + break; + } + } + listener.getLogger().format("Processed %d refs%n", count); + + } + + private void discoverBranches(final Repository repository, + final RevWalk walk, GitSCMSourceRequest request, + Map remoteReferences) + throws IOException, InterruptedException { + listener.getLogger().println("Checking branches..."); + walk.setRetainBody(false); + int count = 0; + for (final Map.Entry ref : remoteReferences.entrySet()) { + if (!ref.getKey().startsWith(Constants.R_HEADS)) { + continue; + } + count++; + final String branchName = StringUtils.removeStart(ref.getKey(), Constants.R_HEADS); + if (request.process(new GitBranchSCMHead(branchName), + new SCMSourceRequest.IntermediateLambda() { + @Nullable + @Override + public ObjectId create() throws IOException, InterruptedException { + listener.getLogger().println(" Checking branch " + branchName); + return ref.getValue(); + } + }, + new SCMSourceRequest.ProbeLambda() { + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull GitBranchSCMHead head, + @Nullable ObjectId revisionInfo) + throws IOException, InterruptedException { + RevCommit commit = walk.parseCommit(revisionInfo); + final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + final RevTree tree = commit.getTree(); + return new TreeWalkingSCMProbe(branchName, lastModified, repository, tree); + } + }, new SCMSourceRequest.LazyRevisionLambda() { + @NonNull + @Override + public SCMRevision create(@NonNull GitBranchSCMHead head, @Nullable ObjectId intermediate) + throws IOException, InterruptedException { + return new GitBranchSCMRevision(head, ref.getValue().name()); + } + }, new SCMSourceRequest.Witness() { + @Override + public void record(@NonNull SCMHead head, SCMRevision revision, boolean isMatch) { + if (isMatch) { + listener.getLogger().println(" Met criteria"); + } else { + listener.getLogger().println(" Does not meet criteria"); + } + } } + )) { + listener.getLogger().format("Processed %d branches (query complete)%n", count); + return; + } + } + listener.getLogger().format("Processed %d branches%n", count); + } - @Override - public boolean exists(@NonNull String path) throws IOException { - TreeWalk tw = TreeWalk.forPath(repository, path, tree); - try { - return tw != null; - } finally { - if (tw != null) { - tw.release(); + private void discoverTags(final Repository repository, + final RevWalk walk, GitSCMSourceRequest request, + Map remoteReferences) + throws IOException, InterruptedException { + listener.getLogger().println("Checking tags..."); + walk.setRetainBody(false); + int count = 0; + for (final Map.Entry ref : remoteReferences.entrySet()) { + if (!ref.getKey().startsWith(Constants.R_TAGS)) { + continue; + } + count++; + final String tagName = StringUtils.removeStart(ref.getKey(), Constants.R_TAGS); + RevCommit commit = walk.parseCommit(ref.getValue()); + final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + if (request.process(new GitTagSCMHead(tagName, lastModified), + new SCMSourceRequest.IntermediateLambda() { + @Nullable + @Override + public ObjectId create() throws IOException, InterruptedException { + listener.getLogger().println(" Checking tag " + tagName); + return ref.getValue(); + } + }, + new SCMSourceRequest.ProbeLambda() { + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull GitTagSCMHead head, + @Nullable ObjectId revisionInfo) + throws IOException, InterruptedException { + RevCommit commit = walk.parseCommit(revisionInfo); + final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + final RevTree tree = commit.getTree(); + return new TreeWalkingSCMProbe(tagName, lastModified, repository, tree); + } + }, new SCMSourceRequest.LazyRevisionLambda() { + @NonNull + @Override + public GitTagSCMRevision create(@NonNull GitTagSCMHead head, @Nullable ObjectId intermediate) + throws IOException, InterruptedException { + return new GitTagSCMRevision(head, ref.getValue().name()); + } + }, new SCMSourceRequest.Witness() { + @Override + public void record(@NonNull SCMHead head, SCMRevision revision, boolean isMatch) { + if (isMatch) { + listener.getLogger().println(" Met criteria"); + } else { + listener.getLogger().println(" Does not meet criteria"); } } } - }; - if (branchCriteria.isHead(probe, listener)) { - listener.getLogger().println("Met criteria"); - } else { - listener.getLogger().println("Does not meet criteria"); - continue; + )) { + listener.getLogger().format("Processed %d tags (query complete)%n", count); + return; + } + } + listener.getLogger().format("Processed %d tags%n", count); + } + }, context, listener, true, true); + } + + /** + * {@inheritDoc} + */ + @CheckForNull + @Override + protected SCMRevision retrieve(@NonNull final String revision, @NonNull final TaskListener listener, @CheckForNull Item retrieveContext) throws IOException, InterruptedException { + + final GitSCMSourceContext context = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()).withTraits(getTraits()); + final GitSCMTelescope telescope = GitSCMTelescope.of(this); + if (telescope != null) { + final String remote = getRemote(); + final StandardUsernameCredentials credentials = getCredentials(retrieveContext); + telescope.validate(remote, credentials); + SCMRevision result = telescope.getRevision(remote, credentials, revision); + if (result != null) { + return result; + } + result = telescope.getRevision(remote, credentials, Constants.R_HEADS + revision); + if (result != null) { + return result; + } + result = telescope.getRevision(remote, credentials, Constants.R_TAGS + revision); + if (result != null) { + return result; + } + return null; + } + // first we need to figure out what the revision is. There are six possibilities: + // 1. A branch name (if we have that we can return quickly) + // 2. A tag name (if we have that we will need to fetch the tag to resolve the tag date) + // 3. A short/full revision hash that is the head revision of a branch (if we have that we can return quickly) + // 4. A remote refspec for example pull-requests/1/from + // 5. A short/full revision hash of a non default ref (non branch or tag but somewhere else under refs/) + // 6. A short revision hash that is the head revision of a branch (if we have that we can return quickly) + // 7. A short/full revision hash for a tag (we'll need to fetch the tag to resolve the tag date) + // 8. A short/full revision hash that is not the head revision of a branch (we'll need to fetch everything to + // try and resolve the hash from the history of one of the heads) + Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)); + GitTool tool = resolveGitTool(context.gitTool(), listener); + if (tool != null) { + git.using(tool.getGitExe()); + } + final GitClient client = git.getClient(); + client.addDefaultCredentials(getCredentials(retrieveContext)); + listener.getLogger().printf("Attempting to resolve %s from remote references...%n", revision); + boolean headsOnly = !context.wantOtherRefs() && context.wantBranches(); + boolean tagsOnly = !context.wantOtherRefs() && context.wantTags(); + Map remoteReferences = client.getRemoteReferences( + getRemote(), null, headsOnly, tagsOnly + ); + String tagName = null; + Set shortNameMatches = new TreeSet<>(); + String shortHashMatch = null; + Set fullTagMatches = new TreeSet<>(); + Set fullHashMatches = new TreeSet<>(); + String fullHashMatch = null; + GitRefSCMRevision candidateOtherRef = null; + for (Map.Entry entry: remoteReferences.entrySet()) { + String name = entry.getKey(); + String rev = entry.getValue().name(); + if ("HEAD".equals(name)) { + //Skip HEAD as it should only appear during testing, not for standard bare repos iirc + continue; + } + if (name.equals(Constants.R_HEADS + revision)) { + listener.getLogger().printf("Found match: %s revision %s%n", name, rev); + // WIN! + return new GitBranchSCMRevision(new GitBranchSCMHead(revision), rev); + } + if (name.equals(Constants.R_TAGS+revision)) { + listener.getLogger().printf("Found match: %s revision %s%n", name, rev); + // WIN but not the good kind + tagName = revision; + context.wantBranches(false); + context.wantTags(true); + context.withoutRefSpecs(); + break; + } + if (name.startsWith(Constants.R_HEADS) && revision.equalsIgnoreCase(rev)) { + listener.getLogger().printf("Found match: %s revision %s%n", name, rev); + // WIN! + return new GitBranchSCMRevision(new GitBranchSCMHead(StringUtils.removeStart(name, Constants.R_HEADS)), rev); + } + if (name.startsWith(Constants.R_TAGS) && revision.equalsIgnoreCase(rev)) { + listener.getLogger().printf("Candidate match: %s revision %s%n", name, rev); + // WIN but let's see if a branch also matches as that would save a fetch + fullTagMatches.add(name); + continue; + } + if((Constants.R_REFS + revision.toLowerCase(Locale.ENGLISH)).equals(name.toLowerCase(Locale.ENGLISH))) { + fullHashMatches.add(name); + if (fullHashMatch == null) { + fullHashMatch = rev; + } + continue; + } + if (rev.toLowerCase(Locale.ENGLISH).equals(revision.toLowerCase(Locale.ENGLISH))) { + fullHashMatches.add(name); + if (fullHashMatch == null) { + fullHashMatch = rev; + } + //Since it was a full match then the shortMatch below will also match, so just skip it + continue; + } + for (GitSCMSourceContext.RefNameMapping o : (Collection)context.getRefNameMappings()) { + if (o.matches(revision, name, rev)) { + candidateOtherRef = new GitRefSCMRevision(new GitRefSCMHead(revision, name), rev); + break; + } + } + if (rev.toLowerCase(Locale.ENGLISH).startsWith(revision.toLowerCase(Locale.ENGLISH))) { + shortNameMatches.add(name); + if (shortHashMatch == null) { + listener.getLogger().printf("Candidate partial match: %s revision %s%n", name, rev); + shortHashMatch = rev; + } else { + listener.getLogger().printf("Candidate partial match: %s revision %s%n", name, rev); + listener.getLogger().printf("Cannot resolve ambiguous short revision %s%n", revision); + return null; + } + } + } + if (!fullTagMatches.isEmpty()) { + // we just want a tag so we can do a minimal fetch + String name = StringUtils.removeStart(fullTagMatches.iterator().next(), Constants.R_TAGS); + listener.getLogger().printf("Selected match: %s revision %s%n", name, shortHashMatch); + tagName = name; + context.wantBranches(false); + context.wantTags(true); + context.withoutRefSpecs(); + } + if (fullHashMatch != null) { + //since this would have been skipped if this was a head or a tag we can just return whatever + return new GitRefSCMRevision(new GitRefSCMHead(fullHashMatch, fullHashMatches.iterator().next()), fullHashMatch); + } + if (shortHashMatch != null) { + // woot this seems unambiguous + for (String name: shortNameMatches) { + if (name.startsWith(Constants.R_HEADS)) { + listener.getLogger().printf("Selected match: %s revision %s%n", name, shortHashMatch); + // WIN it's also a branch + return new GitBranchSCMRevision(new GitBranchSCMHead(StringUtils.removeStart(name, Constants.R_HEADS)), + shortHashMatch); + } else if (name.startsWith(Constants.R_TAGS)) { + tagName = StringUtils.removeStart(name, Constants.R_TAGS); + context.wantBranches(false); + context.wantTags(true); + context.withoutRefSpecs(); + } + } + if (tagName != null) { + listener.getLogger().printf("Selected match: %s revision %s%n", tagName, shortHashMatch); + } else { + return new GitRefSCMRevision(new GitRefSCMHead(shortHashMatch, shortNameMatches.iterator().next()), shortHashMatch); + } + } + if (candidateOtherRef != null) { + return candidateOtherRef; + } + //if PruneStaleBranches it should take affect on the following retrievals + boolean pruneRefs = context.pruneRefs(); + if (tagName != null) { + listener.getLogger().println( + "Resolving tag commit... (remote references may be a lightweight tag or an annotated tag)"); + final String tagRef = Constants.R_TAGS+tagName; + return doRetrieve(new Retriever() { + @Override + public SCMRevision run(GitClient client, String remoteName) throws IOException, + InterruptedException { + try (final Repository repository = client.getRepository(); + RevWalk walk = new RevWalk(repository)) { + ObjectId ref = client.revParse(tagRef); + RevCommit commit = walk.parseCommit(ref); + long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + listener.getLogger().printf("Resolved tag %s revision %s%n", revision, + ref.getName()); + return new GitTagSCMRevision(new GitTagSCMHead(revision, lastModified), + ref.name()); + } + } + }, + context, + listener, pruneRefs); + } + // Pokémon!... Got to catch them all + listener.getLogger().printf("Could not find %s in remote references. " + + "Pulling heads to local for deep search...%n", revision); + context.wantTags(true); + context.wantBranches(true); + + return doRetrieve(new Retriever() { + @Override + public SCMRevision run(GitClient client, String remoteName) throws IOException, InterruptedException { + ObjectId objectId; + String hash; + try { + objectId = client.revParse(revision); + if (objectId == null) { + //just to be safe + listener.error("Could not resolve %s", revision); + return null; + + } + hash = objectId.name(); + String candidatePrefix = Constants.R_REMOTES.substring(Constants.R_REFS.length()) + + context.remoteName() + "/"; + String name = null; + for (Branch b: client.getBranchesContaining(hash, true)) { + if (b.getName().startsWith(candidatePrefix)) { + name = b.getName().substring(candidatePrefix.length()); + break; + } + } + if (name == null) { + listener.getLogger().printf("Could not find a branch containing commit %s%n", + hash); + return null; + } + listener.getLogger() + .printf("Selected match: %s revision %s%n", name, hash); + return new GitBranchSCMRevision(new GitBranchSCMHead(name), hash); + } catch (GitException x) { + x.printStackTrace(listener.error("Could not resolve %s", revision)); + return null; + } + } + }, + context, + listener, pruneRefs); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + protected Set retrieveRevisions(@NonNull final TaskListener listener, @CheckForNull Item retrieveContext) throws IOException, InterruptedException { + + final GitSCMSourceContext context = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()).withTraits(getTraits()); + final GitSCMTelescope telescope = GitSCMTelescope.of(this); + if (telescope != null) { + final String remote = getRemote(); + final StandardUsernameCredentials credentials = getCredentials(retrieveContext); + telescope.validate(remote, credentials); + Set referenceTypes = new HashSet<>(); + if (context.wantBranches()) { + referenceTypes.add(GitSCMTelescope.ReferenceType.HEAD); + } + if (context.wantTags()) { + referenceTypes.add(GitSCMTelescope.ReferenceType.TAG); + } + Set result = new HashSet<>(); + for (SCMRevision r : telescope.getRevisions(remote, credentials, referenceTypes)) { + if (r instanceof GitTagSCMRevision && context.wantTags()) { + result.add(r.getHead().getName()); + } else if (!(r instanceof GitTagSCMRevision) && context.wantBranches()) { + result.add(r.getHead().getName()); + } + } + return result; + } + Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)); + GitTool tool = resolveGitTool(context.gitTool(), listener); + if (tool != null) { + git.using(tool.getGitExe()); + } + GitClient client = git.getClient(); + client.addDefaultCredentials(getCredentials(retrieveContext)); + Set revisions = new HashSet<>(); + if (context.wantBranches() || context.wantTags() || context.wantOtherRefs()) { + listener.getLogger().println("Listing remote references..."); + boolean headsOnly = !context.wantOtherRefs() && context.wantBranches(); + boolean tagsOnly = !context.wantOtherRefs() && context.wantTags(); + Map remoteReferences = client.getRemoteReferences( + getRemote(), null, headsOnly, tagsOnly + ); + for (String name : remoteReferences.keySet()) { + if (context.wantBranches()) { + if (name.startsWith(Constants.R_HEADS)) { + revisions.add(StringUtils.removeStart(name, Constants.R_HEADS)); + } + } + if (context.wantTags()) { + if (name.startsWith(Constants.R_TAGS)) { + revisions.add(StringUtils.removeStart(name, Constants.R_TAGS)); + } + } + if (context.wantOtherRefs() && (!name.startsWith(Constants.R_HEADS) || !name.startsWith(Constants.R_TAGS))) { + for (GitSCMSourceContext.RefNameMapping o : (Collection)context.getRefNameMappings()) { + if (o.matches(name)) { + final String revName = o.getName(name); + if (revName != null) { + revisions.add(revName); + break; + } } } - SCMHead head = new SCMHead(branchName); - SCMRevision hash = new SCMRevisionImpl(head, b.getSHA1String()); - observer.observe(head, hash); - if (!observer.isObserving()) { - return; + } + } + } + return revisions; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + protected List retrieveActions(@CheckForNull SCMSourceEvent event, @NonNull TaskListener listener) + throws IOException, InterruptedException { + final GitSCMTelescope telescope = GitSCMTelescope.of(this); + if (telescope != null) { + final String remote = getRemote(); + final StandardUsernameCredentials credentials = getCredentials(); + telescope.validate(remote, credentials); + String target = telescope.getDefaultTarget(remote, credentials); + if (target.startsWith(Constants.R_HEADS)) { + // shorten standard names + target = target.substring(Constants.R_HEADS.length()); + } + List result = new ArrayList<>(); + if (StringUtils.isNotBlank(target)) { + result.add(new GitRemoteHeadRefAction(getRemote(), target)); + } + return result; + } + final GitSCMSourceContext context = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()).withTraits(getTraits()); + Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)); + GitTool tool = resolveGitTool(context.gitTool(), listener); + if (tool != null) { + git.using(tool.getGitExe()); + } + GitClient client = git.getClient(); + client.addDefaultCredentials(getCredentials()); + Map symrefs = client.getRemoteSymbolicReferences(getRemote(), null); + if (symrefs.containsKey(Constants.HEAD)) { + // Hurrah! The Server is Git 1.8.5 or newer and our client has symref reporting + String target = symrefs.get(Constants.HEAD); + if (target.startsWith(Constants.R_HEADS)) { + // shorten standard names + target = target.substring(Constants.R_HEADS.length()); + } + List result = new ArrayList<>(); + if (StringUtils.isNotBlank(target)) { + result.add(new GitRemoteHeadRefAction(getRemote(), target)); + } + return result; + } + // Ok, now we do it the old-school way... see what ref has the same hash as HEAD + // I think we will still need to keep this code path even if JGit implements + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=514052 as there is always the potential that + // the remote server is Git 1.8.4 or earlier, or that the local CLI git implementation is + // older than git 2.8.0 (CentOS 6, CentOS 7, Debian 7, Debian 8, Ubuntu 14, and + // Ubuntu 16) + Map remoteReferences = client.getRemoteReferences(getRemote(), null, false, false); + if (remoteReferences.containsKey(Constants.HEAD)) { + ObjectId head = remoteReferences.get(Constants.HEAD); + Set names = new TreeSet<>(); + for (Map.Entry entry : remoteReferences.entrySet()) { + if (entry.getKey().equals(Constants.HEAD)) continue; + if (head.equals(entry.getValue())) { + names.add(entry.getKey()); + } + } + // if there is one and only one match, that's the winner + if (names.size() == 1) { + String target = names.iterator().next(); + if (target.startsWith(Constants.R_HEADS)) { + // shorten standard names + target = target.substring(Constants.R_HEADS.length()); + } + List result = new ArrayList<>(); + if (StringUtils.isNotBlank(target)) { + result.add(new GitRemoteHeadRefAction(getRemote(), target)); + } + return result; + } + // if there are multiple matches, prefer `master` + if (names.contains(Constants.R_HEADS + Constants.MASTER)) { + List result = new ArrayList<>(); + result.add(new GitRemoteHeadRefAction(getRemote(), Constants.MASTER)); + return result; + } + } + // Give up, there's no way to get the primary branch + return new ArrayList<>(); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + protected List retrieveActions(@NonNull SCMHead head, @CheckForNull SCMHeadEvent event, + @NonNull TaskListener listener) throws IOException, InterruptedException { + SCMSourceOwner owner = getOwner(); + if (owner instanceof Actionable) { + for (GitRemoteHeadRefAction a: ((Actionable) owner).getActions(GitRemoteHeadRefAction.class)) { + if (getRemote().equals(a.getRemote())) { + if (head.getName().equals(a.getName())) { + return Collections.singletonList(new PrimaryInstanceMetadataAction()); } } - } finally { - walk.dispose(); } + } + return Collections.emptyList(); + } - listener.getLogger().println("Done."); - } finally { - cacheLock.unlock(); + /** + * {@inheritDoc} + */ + @Override + protected boolean isCategoryEnabled(@NonNull SCMHeadCategory category) { + if (super.isCategoryEnabled(category)) { + for (SCMSourceTrait trait : getTraits()) { + if (trait.isCategoryEnabled(category)) { + return true; + } + } } + return false; } protected String getCacheEntry() { - return "git-" + Util.getDigestOf(getRemote()); + return getCacheEntry(getRemote()); } protected static File getCacheDir(String cacheEntry) { - File cacheDir = new File(new File(Jenkins.getInstance().getRootDir(), "caches"), cacheEntry); - cacheDir.getParentFile().mkdirs(); + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + return null; + } + File cacheDir = new File(new File(jenkins.getRootDir(), "caches"), cacheEntry); + if (!cacheDir.isDirectory()) { + boolean ok = cacheDir.mkdirs(); + if (!ok) { + LOGGER.log(Level.WARNING, "Failed mkdirs of {0}", cacheDir); + } + } return cacheDir; } @@ -257,38 +1234,147 @@ protected static Lock getCacheLock(String cacheEntry) { return cacheLock; } + @CheckForNull protected StandardUsernameCredentials getCredentials() { + return getCredentials(getOwner()); + } + + @CheckForNull + private StandardUsernameCredentials getCredentials(@CheckForNull Item context) { + String credentialsId = getCredentialsId(); + if (credentialsId == null) { + return null; + } return CredentialsMatchers .firstOrNull( - CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, getOwner(), + CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, ACL.SYSTEM, URIRequirementBuilder.fromUri(getRemote()).build()), - CredentialsMatchers.allOf(CredentialsMatchers.withId(getCredentialsId()), + CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId), GitClient.CREDENTIALS_MATCHER)); } - protected abstract List getRefSpecs(); + /** + * @return the ref specs. + * @deprecated use {@link RefSpecsSCMSourceTrait} + */ + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + protected List getRefSpecs() { + return Collections.emptyList(); + } + /** + * Instantiates a new {@link GitSCMBuilder}. + * Subclasses should override this method if they want to use a custom {@link GitSCMBuilder} or if they need + * to pre-decorate the builder. + * + * @param head the {@link SCMHead}. + * @param revision the {@link SCMRevision}. + * @return the {@link GitSCMBuilder} + * @see #decorate(GitSCMBuilder) for post-decoration. + */ + protected GitSCMBuilder newBuilder(@NonNull SCMHead head, @CheckForNull SCMRevision revision) { + return new GitSCMBuilder<>(head, revision, getRemote(), getCredentialsId()); + } + + /** + * Performs final decoration of the {@link GitSCMBuilder}. This method is called by + * {@link #build(SCMHead, SCMRevision)} immediately prior to returning {@link GitSCMBuilder#build()}. + * Subclasses should override this method if they need to overrule builder behaviours defined by traits. + * + * @param builder the builder to decorate. + */ + protected void decorate(GitSCMBuilder builder) { + } + + /** + * {@inheritDoc} + */ @NonNull @Override public SCM build(@NonNull SCMHead head, @CheckForNull SCMRevision revision) { - BuildChooser buildChooser = revision instanceof SCMRevisionImpl ? new SpecificRevisionBuildChooser( - (SCMRevisionImpl) revision) : new DefaultBuildChooser(); - return new GitSCM( - getRemoteConfigs(), - Collections.singletonList(new BranchSpec(head.getName())), - false, Collections.emptyList(), - null, null, Collections.singletonList(new BuildChooserSetting(buildChooser))); + GitSCMBuilder builder = newBuilder(head, revision); + if (Util.isOverridden(AbstractGitSCMSource.class, getClass(), "getExtensions")) { + builder.withExtensions(getExtensions()); + } + if (Util.isOverridden(AbstractGitSCMSource.class, getClass(), "getBrowser")) { + builder.withBrowser(getBrowser()); + } + if (Util.isOverridden(AbstractGitSCMSource.class, getClass(), "getGitTool")) { + builder.withGitTool(getGitTool()); + } + if (Util.isOverridden(AbstractGitSCMSource.class, getClass(), "getRefSpecs")) { + List specs = new ArrayList<>(); + for (RefSpec spec: getRefSpecs()) { + specs.add(spec.toString()); + } + builder.withoutRefSpecs().withRefSpecs(specs); + } + builder.withTraits(getTraits()); + decorate(builder); + return builder.build(); } + /** + * @return the {@link UserRemoteConfig} instances. + * @deprecated use {@link GitSCMBuilder#asRemoteConfigs()} + */ + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") protected List getRemoteConfigs() { List refSpecs = getRefSpecs(); - List result = new ArrayList(refSpecs.size()); + List result = new ArrayList<>(refSpecs.size()); String remote = getRemote(); for (RefSpec refSpec : refSpecs) { result.add(new UserRemoteConfig(remote, getRemoteName(), refSpec.toString(), getCredentialsId())); } return result; } + + /** + * Returns true if the branchName isn't matched by includes or is matched by excludes. + * + * @param branchName name of branch to be tested + * @return true if branchName is excluded or is not included + * @deprecated use {@link WildcardSCMSourceFilterTrait} + */ + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + protected boolean isExcluded (String branchName){ + return !Pattern.matches(getPattern(getIncludes()), branchName) || (Pattern.matches(getPattern(getExcludes()), branchName)); + } + + /** + * Returns the pattern corresponding to the branches containing wildcards. + * + * @param branches branch names to evaluate + * @return pattern corresponding to the branches containing wildcards + */ + private String getPattern(String branches){ + StringBuilder quotedBranches = new StringBuilder(); + for (String wildcard : branches.split(" ")){ + StringBuilder quotedBranch = new StringBuilder(); + for(String branch : wildcard.split("(?=[*])|(?<=[*])")){ + if (branch.equals("*")) { + quotedBranch.append(".*"); + } else if (!branch.isEmpty()) { + quotedBranch.append(Pattern.quote(branch)); + } + } + if (quotedBranches.length()>0) { + quotedBranches.append("|"); + } + quotedBranches.append(quotedBranch); + } + return quotedBranches.toString(); + } + + /*package*/ static String getCacheEntry(String remote) { + return "git-" + Util.getDigestOf(remote); + } /** * Our implementation. @@ -298,17 +1384,21 @@ public static class SCMRevisionImpl extends SCMRevision { /** * The subversion revision. */ - private String hash; + private final String hash; public SCMRevisionImpl(SCMHead head, String hash) { super(head); this.hash = hash; } + @Exported public String getHash() { return hash; } + /** + * {@inheritDoc} + */ @Override public boolean equals(Object o) { if (this == o) { @@ -320,14 +1410,26 @@ public boolean equals(Object o) { SCMRevisionImpl that = (SCMRevisionImpl) o; - return StringUtils.equals(hash, that.hash) && getHead().equals(that.getHead()); - + return Objects.equals(hash, that.hash) + && Objects.equals(getHead(), that.getHead()); } + /** + * {@inheritDoc} + */ @Override public int hashCode() { - return hash != null ? hash.hashCode() : 0; + return Objects.hash(hash, getHead()); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return hash; } + } public static class SpecificRevisionBuildChooser extends BuildChooser { @@ -340,6 +1442,9 @@ public SpecificRevisionBuildChooser(SCMRevisionImpl revision) { this.revision = new Revision(sha1, Collections.singleton(new Branch(name, sha1))); } + /** + * {@inheritDoc} + */ @Override public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, GitClient git, TaskListener listener, BuildData buildData, @@ -348,6 +1453,9 @@ public Collection getCandidateRevisions(boolean isPollCall, String sin return Collections.singleton(revision); } + /** + * {@inheritDoc} + */ @Override public Build prevBuildForChangelog(String branch, @Nullable BuildData data, GitClient git, BuildChooserContext context) throws IOException, InterruptedException { @@ -355,19 +1463,203 @@ public Build prevBuildForChangelog(String branch, @Nullable BuildData data, GitC return data == null ? null : data.lastBuild; } - @Extension - public static class DescriptorImpl extends BuildChooserDescriptor { + } + + /** + * A {@link SCMProbe} that uses a local cache of the repository. + * + * @since 3.6.1 + */ + @SuppressFBWarnings(value = { "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE", + "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + "NP_LOAD_OF_KNOWN_NULL_VALUE" + }, + justification = "Java 11 generated code causes redundant nullcheck") + private static class TreeWalkingSCMProbe extends SCMProbe { + private final String name; + private final long lastModified; + private final Repository repository; + private final RevTree tree; + + public TreeWalkingSCMProbe(String name, long lastModified, Repository repository, RevTree tree) { + this.name = name; + this.lastModified = lastModified; + this.repository = repository; + this.tree = tree; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + // no-op + } - @Override - public String getDisplayName() { - return "Specific revision"; + /** + * {@inheritDoc} + */ + @Override + public String name() { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public long lastModified() { + return lastModified; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public SCMProbeStat stat(@NonNull String path) throws IOException { + try (TreeWalk tw = TreeWalk.forPath(repository, path, tree)) { + if (tw == null) { + return SCMProbeStat.fromType(SCMFile.Type.NONEXISTENT); + } + FileMode fileMode = tw.getFileMode(0); + if (fileMode == FileMode.MISSING) { + return SCMProbeStat.fromType(SCMFile.Type.NONEXISTENT); + } + if (fileMode == FileMode.EXECUTABLE_FILE) { + return SCMProbeStat.fromType(SCMFile.Type.REGULAR_FILE); + } + if (fileMode == FileMode.REGULAR_FILE) { + return SCMProbeStat.fromType(SCMFile.Type.REGULAR_FILE); + } + if (fileMode == FileMode.SYMLINK) { + return SCMProbeStat.fromType(SCMFile.Type.LINK); + } + if (fileMode == FileMode.TREE) { + return SCMProbeStat.fromType(SCMFile.Type.DIRECTORY); + } + return SCMProbeStat.fromType(SCMFile.Type.OTHER); } + } + } + + /** + * A {@link SCMProbe} that uses a {@link GitSCMTelescope}. + * + * @since 3.6.1 + */ + private static class TelescopingSCMProbe extends SCMProbe { + /** + * Our telescope. + */ + @NonNull + private final GitSCMTelescope telescope; + /** + * The repository URL. + */ + @NonNull + private final String remote; + /** + * The credentials to use. + */ + @CheckForNull + private final StandardCredentials credentials; + /** + * The revision this probe operates on. + */ + @NonNull + private final SCMRevision revision; + /** + * The filesystem (lazy init). + */ + @GuardedBy("this") + @CheckForNull + private SCMFileSystem fileSystem; + /** + * The last modified timestamp (lazy init). + */ + @GuardedBy("this") + @CheckForNull + private Long lastModified; + + /** + * Constructor. + * @param telescope the telescope. + * @param remote the repository URL + * @param credentials the credentials to use. + * @param revision the revision to probe. + */ + public TelescopingSCMProbe(GitSCMTelescope telescope, String remote, StandardCredentials credentials, + SCMRevision revision) { + this.telescope = telescope; + this.remote = remote; + this.credentials = credentials; + this.revision = revision; + } - public boolean isApplicable(java.lang.Class job) { - return SCMSourceOwner.class.isAssignableFrom(job); + /** + * {@inheritDoc} + */ + @NonNull + @Override + public SCMProbeStat stat(@NonNull String path) throws IOException { + try { + SCMFileSystem fileSystem; + synchronized (this) { + if (this.fileSystem == null) { + this.fileSystem = telescope.build(remote, credentials, revision.getHead(), revision); + } + fileSystem = this.fileSystem; + } + if (fileSystem == null) { + throw new IOException("Cannot connect to " + remote + " as " + + (credentials == null ? "anonymous" : CredentialsNameProvider.name(credentials))); + } + return SCMProbeStat.fromType(fileSystem.child(path).getType()); + } catch (InterruptedException e) { + throw new IOException(e); } + } + /** + * {@inheritDoc} + */ + @Override + public synchronized void close() throws IOException { + if (fileSystem != null) { + fileSystem.close(); + } + fileSystem = null; } + /** + * {@inheritDoc} + */ + @Override + public String name() { + return revision.getHead().getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public long lastModified() { + synchronized (this) { + if (lastModified != null) { + return lastModified; + } + } + long lastModified; + try { + lastModified = telescope.getTimestamp(remote, credentials, revision.getHead()); + } catch (IOException | InterruptedException e) { + return -1L; + } + synchronized (this) { + this.lastModified = lastModified; + } + return lastModified; + } } } diff --git a/src/main/java/jenkins/plugins/git/GitBranchSCMHead.java b/src/main/java/jenkins/plugins/git/GitBranchSCMHead.java new file mode 100644 index 0000000000..8de9e378b9 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitBranchSCMHead.java @@ -0,0 +1,87 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadMigration; +import jenkins.scm.api.SCMRevision; +import org.eclipse.jgit.lib.Constants; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +public class GitBranchSCMHead extends SCMHead implements GitSCMHeadMixin { + /** + * Constructor. + * + * @param name the name. + */ + public GitBranchSCMHead(@NonNull String name) { + super(name); + } + + @Override + public final String getRef() { + return Constants.R_HEADS + getName(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("GitBranchSCMHead{"); + sb.append("name='").append(getName()).append("'"); + sb.append(", ref='").append(getRef()).append("'}"); + return sb.toString(); + } + + @Restricted(NoExternalUse.class) + @Extension + public static class SCMHeadMigrationImpl extends SCMHeadMigration { + + public SCMHeadMigrationImpl() { + super(GitSCMSource.class, SCMHead.class, AbstractGitSCMSource.SCMRevisionImpl.class); + } + + @Override + public SCMHead migrate(@NonNull GitSCMSource source, @NonNull SCMHead head) { + return new GitBranchSCMHead(head.getName()); + } + + @Override + public SCMRevision migrate(@NonNull GitSCMSource source, @NonNull AbstractGitSCMSource.SCMRevisionImpl revision) { + if (revision.getHead().getClass() == SCMHead.class) { + SCMHead revisionHead = revision.getHead(); + SCMHead branchHead = migrate(source, revisionHead); + if (branchHead != null) { + GitBranchSCMHead gitBranchHead = (GitBranchSCMHead) branchHead; + String revisionHash = revision.getHash(); + return new GitBranchSCMRevision(gitBranchHead, revisionHash); + } + } + return null; + } + + } +} diff --git a/src/main/java/jenkins/plugins/git/GitBranchSCMRevision.java b/src/main/java/jenkins/plugins/git/GitBranchSCMRevision.java new file mode 100644 index 0000000000..486296f02e --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitBranchSCMRevision.java @@ -0,0 +1,35 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package jenkins.plugins.git; + + +public class GitBranchSCMRevision extends AbstractGitSCMSource.SCMRevisionImpl { + + public GitBranchSCMRevision(GitBranchSCMHead head, String hash) { + super(head, hash); + } + + +} diff --git a/src/main/java/jenkins/plugins/git/GitRefSCMHead.java b/src/main/java/jenkins/plugins/git/GitRefSCMHead.java new file mode 100644 index 0000000000..b79281b4f1 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitRefSCMHead.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.SCMHead; + +public class GitRefSCMHead extends SCMHead implements GitSCMHeadMixin { + private final String ref; + + /** + * Constructor. + * + * @param name the name of the ref. + * @param ref the ref. + */ + public GitRefSCMHead(@NonNull String name, @NonNull String ref) { + super(name); + this.ref = ref; + } + + /** + * Constructor where ref and name are the same. + * + * @param name the name (and the ref). + */ + public GitRefSCMHead(@NonNull String name) { + this(name, name); + } + + @Override + public String getRef() { + return ref; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("GitRefSCMHead{"); + sb.append("name='").append(getName()).append("'"); + sb.append(", ref='").append(ref).append("'}"); + return sb.toString(); + } +} diff --git a/src/main/java/jenkins/plugins/git/GitRefSCMRevision.java b/src/main/java/jenkins/plugins/git/GitRefSCMRevision.java new file mode 100644 index 0000000000..8ff543f944 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitRefSCMRevision.java @@ -0,0 +1,32 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package jenkins.plugins.git; + +public class GitRefSCMRevision extends AbstractGitSCMSource.SCMRevisionImpl { + + public GitRefSCMRevision(GitRefSCMHead head, String hash) { + super(head, hash); + } +} diff --git a/src/main/java/jenkins/plugins/git/GitRemoteHeadRefAction.java b/src/main/java/jenkins/plugins/git/GitRemoteHeadRefAction.java new file mode 100644 index 0000000000..4bf85259c0 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitRemoteHeadRefAction.java @@ -0,0 +1,64 @@ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.InvisibleAction; +import java.io.Serializable; +import java.util.Objects; + +/** + * @author Stephen Connolly + */ +public class GitRemoteHeadRefAction extends InvisibleAction implements Serializable { + + private static final long serialVersionUID = 1L; + + @NonNull + private final String remote; + @NonNull + private final String name; + + public GitRemoteHeadRefAction(@NonNull String remote, @NonNull String name) { + this.remote = remote; + this.name = name; + } + + @NonNull + public String getRemote() { + return remote; + } + + @NonNull + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + GitRemoteHeadRefAction that = (GitRemoteHeadRefAction) o; + + return Objects.equals(remote, that.remote) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(remote, name); + } + + @Override + public String toString() { + return "GitRemoteHeadRefAction{" + + "remote='" + remote + '\'' + + ", name='" + name + '\'' + + '}'; + } + + +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMBuilder.java b/src/main/java/jenkins/plugins/git/GitSCMBuilder.java new file mode 100644 index 0000000000..61aab9e991 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMBuilder.java @@ -0,0 +1,612 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTool; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.plugins.git.extensions.impl.CloneOption; +import hudson.scm.SCM; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.TagSCMHead; +import jenkins.scm.api.trait.SCMBuilder; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.transport.RefSpec; + +/** + * The {@link SCMBuilder} base class for {@link AbstractGitSCMSource}. + * + * @param the concrete type of {@link GitSCMBuilder} so that subclasses can chain correctly in their + * {@link #withHead(SCMHead)} etc methods. + * @since 3.4.0 + */ +public class GitSCMBuilder> extends SCMBuilder { + + /** + * The {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + */ + @CheckForNull + private GitRepositoryBrowser browser; + /** + * The {@link GitSCMExtension} instances to apply to the {@link GitSCM}. + */ + @NonNull + private final List extensions = new ArrayList<>(); + /** + * The {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to the {@link #remote} or + * {@code null} to let the git client choose between providing its own credentials or connecting anonymously. + */ + @CheckForNull + private String credentialsId; + /** + * The name of the {@link GitTool} to use or {@code null} to use the default. + */ + @CheckForNull + private String gitTool; + /** + * The ref specs to apply to the {@link GitSCM}. + */ + @NonNull + private List refSpecs = new ArrayList<>(); + /** + * The name of the remote, defaults to {@link AbstractGitSCMSource#DEFAULT_REMOTE_NAME}. + */ + @NonNull + private String remoteName = AbstractGitSCMSource.DEFAULT_REMOTE_NAME; + /** + * The remote URL of the git repository. + */ + @NonNull + private String remote; + /** + * Any additional remotes keyed by their remote name. + */ + @NonNull + private final Map additionalRemotes = new TreeMap<>(); + + /** + * Constructor. + * + * @param head The {@link SCMHead} to produce the {@link SCM} for. + * @param revision The {@link SCMRevision} to produce the {@link SCM} for or {@code null} to produce the + * {@link SCM} for the head revision. + * @param remote The remote URL of the git server. + * @param credentialsId The {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #remote} or {@code null} to let the git client choose between providing its own + * credentials or connecting anonymously. + */ + public GitSCMBuilder(@NonNull SCMHead head, @CheckForNull SCMRevision revision, @NonNull String remote, + @CheckForNull String credentialsId) { + super(GitSCM.class, head, revision); + this.remote = remote; + this.credentialsId = credentialsId; + } + + /** + * Returns the {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + * + * @return The {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + */ + @CheckForNull + public final GitRepositoryBrowser browser() { + return browser; + } + + /** + * Returns the {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #remote} or {@code null} to let the git client choose between providing its own + * credentials or connecting anonymously. + * + * @return the {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #remote} or {@code null} to let the git client choose between providing its own + * credentials or connecting anonymously. + */ + @CheckForNull + public final String credentialsId() { + return credentialsId; + } + + /** + * Returns the {@link GitSCMExtension} instances to apply to the {@link GitSCM}. + * + * @return the {@link GitSCMExtension} instances to apply to the {@link GitSCM}. + */ + @NonNull + public final List extensions() { + return Collections.unmodifiableList(extensions); + } + + /** + * Returns the name of the {@link GitTool} to use or {@code null} to use the default. + * + * @return the name of the {@link GitTool} to use or {@code null} to use the default. + */ + @CheckForNull + public final String gitTool() { + return gitTool; + } + + /** + * Returns the list of ref specs to use. + * + * @return the list of ref specs to use. + */ + @NonNull + public final List refSpecs() { + if (refSpecs.isEmpty()) { + return Collections.singletonList(AbstractGitSCMSource.REF_SPEC_DEFAULT); + } + return Collections.unmodifiableList(refSpecs); + } + + /** + * Returns the remote URL of the git repository. + * + * @return the remote URL of the git repository. + */ + @NonNull + public final String remote() { + return remote; + } + + /** + * Returns the name to give the remote. + * + * @return the name to give the remote. + */ + @NonNull + public final String remoteName() { + return remoteName; + } + + /** + * Gets the (possibly empty) additional remote names. + * + * @return the (possibly empty) additional remote names. + */ + @NonNull + public final Set additionalRemoteNames() { + return Collections.unmodifiableSet(additionalRemotes.keySet()); + } + + /** + * Gets the remote URL of the git repository for the specified remote name. + * + * @param remoteName the additional remote name. + * @return the remote URL of the named additional remote or {@code null} if the supplied name is not in + * {@link #additionalRemoteNames()} + */ + @CheckForNull + public final String additionalRemote(String remoteName) { + AdditionalRemote additionalRemote = additionalRemotes.get(remoteName); + return additionalRemote == null ? null : additionalRemote.remote(); + } + + /** + * Gets the ref specs to use for the git repository of the specified remote name. + * + * @param remoteName the additional remote name. + * @return the ref specs for the named additional remote or {@code null} if the supplied name is not in + * {@link #additionalRemoteNames()} + */ + @CheckForNull + public final List additionalRemoteRefSpecs(String remoteName) { + AdditionalRemote additionalRemote = additionalRemotes.get(remoteName); + return additionalRemote == null ? null : additionalRemote.refSpecs(); + } + + /** + * Configures the {@link GitRepositoryBrowser} to use. + * + * @param browser the {@link GitRepositoryBrowser} or {@code null} to use the default "auto" browser. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withBrowser(@CheckForNull GitRepositoryBrowser browser) { + this.browser = browser; + return (B) this; + } + + /** + * Configures the {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to the + * {@link #remote()} + * + * @param credentialsId the {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #remote()} or {@code null} to let the git client choose between providing its own + * credentials or connecting anonymously. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withCredentials(@CheckForNull String credentialsId) { + this.credentialsId = credentialsId; + return (B) this; + } + + /** + * Adds (or redefines) the supplied {@link GitSCMExtension}. + * + * @param extension the {@link GitSCMExtension} ({@code null} values are safely ignored). + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withExtension(@CheckForNull GitSCMExtension extension) { + if (extension != null) { + // the extensions only allow one of each type. + for (Iterator iterator = extensions.iterator(); iterator.hasNext(); ) { + if (extension.getClass().equals(iterator.next().getClass())) { + iterator.remove(); + } + } + extensions.add(extension); + } + return (B) this; + } + + /** + * Adds (or redefines) the supplied {@link GitSCMExtension}s. + * + * @param extensions the {@link GitSCMExtension}s. + * @return {@code this} for method chaining. + */ + @NonNull + public final B withExtensions(GitSCMExtension... extensions) { + return withExtensions(Arrays.asList(extensions)); + } + + /** + * Adds (or redefines) the supplied {@link GitSCMExtension}s. + * + * @param extensions the {@link GitSCMExtension}s. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withExtensions(@NonNull List extensions) { + for (GitSCMExtension extension : extensions) { + withExtension(extension); + } + return (B) this; + } + + /** + * Configures the {@link GitTool#getName()} to use. + * + * @param gitTool the {@link GitTool#getName()} or {@code null} to use the system default. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withGitTool(@CheckForNull String gitTool) { + this.gitTool = gitTool; + return (B) this; + } + + /** + * Adds the specified ref spec. If no ref specs were previously defined then the supplied ref spec will replace + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT}. The ref spec is expected to be processed for substitution of + * {@link AbstractGitSCMSource#REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR} by {@link #remote()} before use. + * + * @param refSpec the ref spec template to add. + * @return {@code this} for method chaining. + * @see #withoutRefSpecs() + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withRefSpec(@NonNull String refSpec) { + this.refSpecs.add(refSpec); + return (B) this; + } + + /** + * Adds the specified ref specs. If no ref specs were previously defined then the supplied ref specs will replace + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT}. The ref spec is expected to be processed for substitution of + * {@link AbstractGitSCMSource#REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR} by {@link #remote()} before use. + * + * @param refSpecs the ref spec templates to add. + * @return {@code this} for method chaining. + * @see #withoutRefSpecs() + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withRefSpecs(@NonNull List refSpecs) { + this.refSpecs.addAll(refSpecs); + return (B) this; + } + + /** + * Clears the specified ref specs. If no ref specs are subsequently defined then + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT} will be used as the ref spec template. + * + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withoutRefSpecs() { + this.refSpecs.clear(); + return (B) this; + } + + /** + * Replaces the URL of the git repository. + * + * @param remote the new URL to use for the git repository. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withRemote(@NonNull String remote) { + this.remote = remote; + return (B) this; + } + + /** + * Configures the remote name to use for the git repository. + * + * @param remoteName the remote name to use for the git repository ({@code null} or the empty string are + * equivalent to passing {@link AbstractGitSCMSource#DEFAULT_REMOTE_NAME}). + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withRemoteName(@CheckForNull String remoteName) { + this.remoteName = StringUtils.defaultIfBlank(remoteName, AbstractGitSCMSource.DEFAULT_REMOTE_NAME); + return (B) this; + } + + /** + * Configures an additional remote. It is the responsibility of the caller to ensure that there are no conflicts + * with the eventual {@link #remote()} name. + * + * @param remoteName the name of the additional remote. + * @param remote the url of the additional remote. + * @param refSpecs the ref specs of the additional remote, if empty will default to + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT} + * @return {@code this} for method chaining. + */ + @NonNull + public final B withAdditionalRemote(@NonNull String remoteName, @NonNull String remote, String... refSpecs) { + return withAdditionalRemote(remoteName, remote, Arrays.asList(refSpecs)); + } + + /** + * Configures an additional remote. It is the responsibility of the caller to ensure that there are no conflicts + * with the eventual {@link #remote()} name. + * + * @param remoteName the name of the additional remote. + * @param remote the url of the additional remote. + * @param refSpecs the ref specs of the additional remote, if empty will default to + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT} + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final B withAdditionalRemote(@NonNull String remoteName, @NonNull String remote, List refSpecs) { + this.additionalRemotes.put(remoteName, new AdditionalRemote(remoteName, remote, refSpecs)); + return (B) this; + } + + /** + * Converts the ref spec templates into {@link RefSpec} instances. + * + * @return the list of {@link RefSpec} instances. + */ + @NonNull + public final List asRefSpecs() { + // de-duplicate effective ref-specs after substitution of placeholder + Set refSpecs = new LinkedHashSet<>(Math.max(this.refSpecs.size(), 1)); + for (String template : refSpecs()) { + refSpecs.add(template.replaceAll(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER, remoteName())); + } + List result = new ArrayList<>(refSpecs.size()); + for (String refSpec : refSpecs) { + result.add(new RefSpec(refSpec)); + } + return result; + } + + /** + * Converts the {@link #asRefSpecs()} into {@link UserRemoteConfig} instances. + * + * @return the list of {@link UserRemoteConfig} instances. + */ + @NonNull + public final List asRemoteConfigs() { + List result = new ArrayList<>(1 + additionalRemotes.size()); + result.add(new UserRemoteConfig(remote(), remoteName(), joinRefSpecs(asRefSpecs()), credentialsId())); + for (AdditionalRemote r : additionalRemotes.values()) { + result.add(new UserRemoteConfig(r.remote(), r.remoteName(), joinRefSpecs(r.asRefSpecs()), credentialsId())); + } + return result; + } + + private String joinRefSpecs(List refSpecs) { + if (refSpecs.isEmpty()) { + return ""; + } + if (refSpecs.size() == 1) { + return refSpecs.get(0).toString(); + } + StringBuilder result = new StringBuilder(refSpecs.size() * 50 /*most ref specs are ~50 chars*/); + boolean first = true; + for (RefSpec r : refSpecs) { + if (first) { + first = false; + } else { + result.append(' '); + } + result.append(r.toString()); + } + return result.toString(); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public GitSCM build() { + List extensions = new ArrayList<>(extensions()); + boolean foundClone = false; + for (GitSCMExtension e: extensions) { + if (e instanceof CloneOption) { + foundClone = true; + break; + } + } + if (!foundClone) { + // assume honour refspecs unless the clone option is added + // TODO revisit once we have support for TagSCMHead implemented as may need to check refspec handling then + extensions.add(new GitSCMSourceDefaults(head() instanceof TagSCMHead)); + } + SCMRevision revision = revision(); + if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { + // remove any conflicting BuildChooserSetting if present + for (Iterator iterator = extensions.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof BuildChooserSetting) { + iterator.remove(); + } + } + extensions.add(new BuildChooserSetting(new AbstractGitSCMSource.SpecificRevisionBuildChooser( + (AbstractGitSCMSource.SCMRevisionImpl) revision))); + } + if (head() instanceof GitRefSCMHead) { + withRefSpec(((GitRefSCMHead) head()).getRef()); + } + return new GitSCM( + asRemoteConfigs(), + Collections.singletonList(new BranchSpec(head().getName())), + false, Collections.emptyList(), + browser(), gitTool(), + extensions); + } + + /** + * Internal value class to manage additional remote configuration. + */ + private static final class AdditionalRemote { + /** + * The name of the remote. + */ + @NonNull + private final String name; + /** + * The url of the remote. + */ + @NonNull + private final String url; + /** + * The ref spec templates of the remote. + */ + @NonNull + private final List refSpecs; + + /** + * Constructor. + * + * @param name the name of the remote. + * @param url the url of the remote. + * @param refSpecs the ref specs of the remote. + */ + public AdditionalRemote(@NonNull String name, @NonNull String url, @NonNull List refSpecs) { + this.name = name; + this.url = url; + this.refSpecs = new ArrayList<>( + refSpecs.isEmpty() + ? Collections.singletonList(AbstractGitSCMSource.REF_SPEC_DEFAULT) + : refSpecs + ); + } + + /** + * Gets the name of the remote. + * + * @return the name of the remote. + */ + public String remoteName() { + return name; + } + + /** + * Gets the url of the remote. + * + * @return the url of the remote. + */ + public String remote() { + return url; + } + + /** + * Gets the ref specs of the remote. + * + * @return the ref specs of the remote. + */ + public List refSpecs() { + return Collections.unmodifiableList(refSpecs); + } + + /** + * Converts the ref spec templates into {@link RefSpec} instances. + * + * @return the list of {@link RefSpec} instances. + */ + @NonNull + public final List asRefSpecs() { + // de-duplicate effective ref-specs after substitution of placeholder + Set refSpecs = new LinkedHashSet<>(Math.max(this.refSpecs.size(), 1)); + for (String template : refSpecs()) { + refSpecs.add(template.replaceAll(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER, remoteName())); + } + List result = new ArrayList<>(refSpecs.size()); + for (String refSpec : refSpecs) { + result.add(new RefSpec(refSpec)); + } + return result; + } + } + +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMFile.java b/src/main/java/jenkins/plugins/git/GitSCMFile.java new file mode 100644 index 0000000000..58dc99bfc3 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMFile.java @@ -0,0 +1,180 @@ +/* + * The MIT License + * + * Copyright (c) 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import jenkins.scm.api.SCMFile; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Implementation of {@link SCMFile} for Git. + * + * @since 3.0.2 + */ +@SuppressFBWarnings(value = { "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE", + "NP_LOAD_OF_KNOWN_NULL_VALUE" + }, + justification = "Java 11 generated code causes redundant nullcheck") +public class GitSCMFile extends SCMFile { + + private final GitSCMFileSystem fs; + + public GitSCMFile(GitSCMFileSystem fs) { + this.fs = fs; + } + + public GitSCMFile(GitSCMFileSystem fs, @NonNull GitSCMFile parent, String name) { + super(parent, name); + this.fs = fs; + } + + @NonNull + @Override + protected SCMFile newChild(String name, boolean assumeIsDirectory) { + return new GitSCMFile(fs, this, name); + } + + @NonNull + @Override + public Iterable children() throws IOException, InterruptedException { + return fs.invoke((Repository repository) -> { + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(fs.getCommitId()); + RevTree tree = commit.getTree(); + if (isRoot()) { + try (TreeWalk tw = new TreeWalk(repository)) { + tw.addTree(tree); + tw.setRecursive(false); + List result = new ArrayList<>(); + while (tw.next()) { + result.add(new GitSCMFile(fs, GitSCMFile.this, tw.getNameString())); + } + return result; + } + } else { + try (TreeWalk tw = TreeWalk.forPath(repository, getPath(), tree)) { + if (tw == null) { + throw new FileNotFoundException(); + } + FileMode fileMode = tw.getFileMode(0); + if (fileMode == FileMode.MISSING) { + throw new FileNotFoundException(); + } + if (fileMode != FileMode.TREE) { + throw new IOException("Not a directory"); + } + tw.enterSubtree(); + List result = new ArrayList<>(); + while (tw.next()) { + result.add(new GitSCMFile(fs, GitSCMFile.this, tw.getNameString())); + } + return result; + } + } + } + }); + } + + @Override + public long lastModified() throws IOException, InterruptedException { + // TODO a more correct implementation + return fs.lastModified(); + } + + @NonNull + @Override + protected Type type() throws IOException, InterruptedException { + return fs.invoke((Repository repository) -> { + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(fs.getCommitId()); + RevTree tree = commit.getTree(); + try (TreeWalk tw = TreeWalk.forPath(repository, getPath(), tree)) { + if (tw == null) { + return SCMFile.Type.NONEXISTENT; + } + FileMode fileMode = tw.getFileMode(0); + if (fileMode == FileMode.MISSING) { + return SCMFile.Type.NONEXISTENT; + } + if (fileMode == FileMode.EXECUTABLE_FILE) { + return SCMFile.Type.REGULAR_FILE; + } + if (fileMode == FileMode.REGULAR_FILE) { + return SCMFile.Type.REGULAR_FILE; + } + if (fileMode == FileMode.SYMLINK) { + return SCMFile.Type.LINK; + } + if (fileMode == FileMode.TREE) { + return SCMFile.Type.DIRECTORY; + } + return SCMFile.Type.OTHER; + } + } + }); + } + + @NonNull + @Override + public InputStream content() throws IOException, InterruptedException { + return fs.invoke((Repository repository) -> { + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(fs.getCommitId()); + RevTree tree = commit.getTree(); + try (TreeWalk tw = TreeWalk.forPath(repository, getPath(), tree)) { + if (tw == null) { + throw new FileNotFoundException(); + } + FileMode fileMode = tw.getFileMode(0); + if (fileMode == FileMode.MISSING) { + throw new FileNotFoundException(); + } + if (fileMode == FileMode.TREE) { + throw new IOException("Directory"); + } + ObjectId objectId = tw.getObjectId(0); + ObjectLoader loader = repository.open(objectId); + return new ByteArrayInputStream(loader.getBytes()); + } + } + }); + } +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMFileSystem.java b/src/main/java/jenkins/plugins/git/GitSCMFileSystem.java new file mode 100644 index 0000000000..3234701509 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMFileSystem.java @@ -0,0 +1,404 @@ +/* + * The MIT License + * + * Copyright (c) 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.Item; +import hudson.model.TaskListener; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTool; +import hudson.plugins.git.UserRemoteConfig; +import hudson.remoting.VirtualChannel; +import hudson.scm.SCM; +import hudson.scm.SCMDescriptor; +import hudson.security.ACL; +import hudson.util.LogTaskListener; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.scm.api.SCMFile; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceDescriptor; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.plugins.gitclient.ChangelogCommand; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +/** + * Base implementation of {@link SCMFileSystem}. + * + * @since 3.0.2 + */ +public class GitSCMFileSystem extends SCMFileSystem { + + /** + * Our logger. + */ + private static final Logger LOGGER = Logger.getLogger(GitSCMFileSystem.class.getName()); + + private final String cacheEntry; + private final TaskListener listener; + private final String remote; + private final String head; + private final GitClient client; + private final ObjectId commitId; + + /** + * Constructor. + * + * @param client the client + * @param remote the remote GIT URL + * @param head identifier for the head commit to be referenced + * @param rev the revision. + * @throws IOException on I/O error + * @throws InterruptedException on thread interruption + */ + protected GitSCMFileSystem(GitClient client, String remote, final String head, @CheckForNull + AbstractGitSCMSource.SCMRevisionImpl rev) throws IOException, InterruptedException { + super(rev); + this.remote = remote; + this.head = head; + cacheEntry = AbstractGitSCMSource.getCacheEntry(remote); + listener = new LogTaskListener(LOGGER, Level.FINER); + this.client = client; + commitId = rev == null ? invoke((Repository repository) -> repository.findRef(head).getObjectId()) : ObjectId.fromString(rev.getHash()); + } + + @Override + public AbstractGitSCMSource.SCMRevisionImpl getRevision() { + return (AbstractGitSCMSource.SCMRevisionImpl) super.getRevision(); + } + + @Override + public long lastModified() throws IOException, InterruptedException { + return invoke((Repository repository) -> { + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(commitId); + return TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + } + }); + } + + @NonNull + @Override + public SCMFile getRoot() { + return new GitSCMFile(this); + } + + /*package*/ ObjectId getCommitId() { + return commitId; + } + + /** + * Called with an {@link FSFunction} callback with a singleton repository + * cache lock. + * + * An example usage might be: + * + *

{@code
+     *      return fs.invoke(new GitSCMFileSystem.FSFunction() {
+     *          public byte[] invoke(Repository repository) throws IOException, InterruptedException {
+     *              Git activeRepo = getClonedRepository(repository);
+     *              File repoDir = activeRepo.getRepository().getDirectory().getParentFile();
+     *              System.out.println("Repo cloned to: " + repoDir.getCanonicalPath());
+     *              try {
+     *                  File f = new File(repoDir, filePath);
+     *                  if (f.canRead()) {
+     *                      return IOUtils.toByteArray(new FileInputStream(f));
+     *                  }
+     *                  return null;
+     *              } finally {
+     *                  FileUtils.deleteDirectory(repoDir);
+     *              }
+     *          }
+     *      });
+     * }
+ * + * @param return type + * @param function callback executed with a locked repository + * @return whatever you return from the provided function + * @throws IOException if there is an I/O error + * @throws InterruptedException if interrupted + */ + public V invoke(final FSFunction function) throws IOException, InterruptedException { + Lock cacheLock = AbstractGitSCMSource.getCacheLock(cacheEntry); + cacheLock.lock(); + try { + File cacheDir = AbstractGitSCMSource.getCacheDir(cacheEntry); + if (cacheDir == null || !cacheDir.isDirectory()) { + throw new IOException("Closed"); + } + return client.withRepository((Repository repository, VirtualChannel virtualChannel) -> function.invoke(repository)); + } finally { + cacheLock.unlock(); + } + } + + @Override + public boolean changesSince(@CheckForNull SCMRevision revision, @NonNull OutputStream changeLogStream) + throws UnsupportedOperationException, IOException, InterruptedException { + AbstractGitSCMSource.SCMRevisionImpl rev = getRevision(); + if (rev == null ? revision == null : rev.equals(revision)) { + // special case where somebody is asking one of two stupid questions: + // 1. what has changed between the latest and the latest + // 2. what has changed between the current revision and the current revision + return false; + } + Lock cacheLock = AbstractGitSCMSource.getCacheLock(cacheEntry); + cacheLock.lock(); + try { + File cacheDir = AbstractGitSCMSource.getCacheDir(cacheEntry); + if (cacheDir == null || !cacheDir.isDirectory()) { + throw new IOException("Closed"); + } + boolean executed = false; + ChangelogCommand changelog = client.changelog(); + try (Writer out = new OutputStreamWriter(changeLogStream, "UTF-8")) { + changelog.includes(commitId); + ObjectId fromCommitId; + if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { + fromCommitId = ObjectId.fromString(((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash()); + changelog.excludes(fromCommitId); + } else { + fromCommitId = null; + } + changelog.to(out).max(GitSCM.MAX_CHANGELOG).execute(); + executed = true; + return !commitId.equals(fromCommitId); + } catch (GitException ge) { + throw new IOException("Unable to retrieve changes", ge); + } finally { + if (!executed) { + changelog.abort(); + } + changeLogStream.close(); + } + } finally { + cacheLock.unlock(); + } + } + + /** + * Simple callback that is used with + * {@link #invoke(jenkins.plugins.git.GitSCMFileSystem.FSFunction)} + * in order to provide a locked view of the Git repository + * @param the return type + */ + public interface FSFunction { + /** + * Called with a lock on the repository in order to perform some + * operations that might result in changes and necessary re-indexing + * @param repository the bare git repository + * @return value to return from {@link #invoke(jenkins.plugins.git.GitSCMFileSystem.FSFunction)} + * @throws IOException if there is an I/O error + * @throws InterruptedException if interrupted + */ + V invoke(Repository repository) throws IOException, InterruptedException; + } + + @Extension(ordinal = Short.MIN_VALUE) + public static class BuilderImpl extends SCMFileSystem.Builder { + + @Override + public boolean supports(SCM source) { + return source instanceof GitSCM + && ((GitSCM) source).getUserRemoteConfigs().size() == 1 + && ((GitSCM) source).getBranches().size() == 1 + && ((GitSCM) source).getBranches().get(0).getName().matches( + "^((\\Q" + Constants.R_HEADS + "\\E.*)|([^/]+)|(\\*/[^/*]+(/[^/*]+)*))$" + ); + // we only support where the branch spec is obvious + } + + @Override + public boolean supports(SCMSource source) { + return source instanceof AbstractGitSCMSource; + } + + @Override + public boolean supportsDescriptor(SCMDescriptor descriptor) { + return descriptor instanceof GitSCM.DescriptorImpl; + } + + @Override + public boolean supportsDescriptor(SCMSourceDescriptor descriptor) { + return AbstractGitSCMSource.class.isAssignableFrom(descriptor.clazz); + } + + @Override + public SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, @CheckForNull SCMRevision rev) + throws IOException, InterruptedException { + if (rev != null && !(rev instanceof AbstractGitSCMSource.SCMRevisionImpl)) { + return null; + } + GitSCM gitSCM = (GitSCM) scm; + UserRemoteConfig config = gitSCM.getUserRemoteConfigs().get(0); + BranchSpec branchSpec = gitSCM.getBranches().get(0); + String remote = config.getUrl(); + TaskListener listener = new LogTaskListener(LOGGER, Level.FINE); + if (remote == null) { + listener.getLogger().println("Git remote url is null"); + return null; + } + String cacheEntry = AbstractGitSCMSource.getCacheEntry(remote); + Lock cacheLock = AbstractGitSCMSource.getCacheLock(cacheEntry); + cacheLock.lock(); + try { + File cacheDir = AbstractGitSCMSource.getCacheDir(cacheEntry); + Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)).in(cacheDir); + GitTool tool = gitSCM.resolveGitTool(listener); + if (tool != null) { + git.using(tool.getGitExe()); + } + GitClient client = git.getClient(); + String credentialsId = config.getCredentialsId(); + if (credentialsId != null) { + StandardCredentials credential = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, + owner, + ACL.SYSTEM, + URIRequirementBuilder.fromUri(remote).build() + ), + CredentialsMatchers.allOf( + CredentialsMatchers.withId(credentialsId), + GitClient.CREDENTIALS_MATCHER + ) + ); + client.addDefaultCredentials(credential); + CredentialsProvider.track(owner, credential); + } + + if (!client.hasGitRepo()) { + listener.getLogger().println("Creating git repository in " + cacheDir); + client.init(); + } + String remoteName = StringUtils.defaultIfBlank(config.getName(), Constants.DEFAULT_REMOTE_NAME); + listener.getLogger().println("Setting " + remoteName + " to " + remote); + client.setRemoteUrl(remoteName, remote); + listener.getLogger().println("Fetching & pruning " + remoteName + "..."); + URIish remoteURI = null; + try { + remoteURI = new URIish(remoteName); + } catch (URISyntaxException ex) { + listener.getLogger().println("URI syntax exception for '" + remoteName + "' " + ex); + } + String headName; + if (rev != null) { + headName = rev.getHead().getName(); + } else { + if (branchSpec.getName().startsWith(Constants.R_HEADS)) { + headName = branchSpec.getName().substring(Constants.R_HEADS.length()); + } else if (branchSpec.getName().startsWith("*/")) { + headName = branchSpec.getName().substring(2); + } else { + headName = branchSpec.getName(); + } + } + client.fetch_().prune(true).from(remoteURI, Arrays + .asList(new RefSpec( + "+" + Constants.R_HEADS + headName + ":" + Constants.R_REMOTES + remoteName + "/" + + headName))).execute(); + listener.getLogger().println("Done."); + return new GitSCMFileSystem(client, remote, Constants.R_REMOTES + remoteName + "/" +headName, (AbstractGitSCMSource.SCMRevisionImpl) rev); + } finally { + cacheLock.unlock(); + } + } + + @Override + public SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @CheckForNull SCMRevision rev) + throws IOException, InterruptedException { + if (rev != null && !(rev instanceof AbstractGitSCMSource.SCMRevisionImpl)) { + return null; + } + TaskListener listener = new LogTaskListener(LOGGER, Level.FINE); + AbstractGitSCMSource gitSCMSource = (AbstractGitSCMSource) source; + GitSCMBuilder builder = gitSCMSource.newBuilder(head, rev); + String cacheEntry = gitSCMSource.getCacheEntry(); + Lock cacheLock = AbstractGitSCMSource.getCacheLock(cacheEntry); + cacheLock.lock(); + try { + File cacheDir = AbstractGitSCMSource.getCacheDir(cacheEntry); + Git git = Git.with(listener, new EnvVars(EnvVars.masterEnvVars)).in(cacheDir); + GitTool tool = gitSCMSource.resolveGitTool(builder.gitTool(), listener); + if (tool != null) { + git.using(tool.getGitExe()); + } + GitClient client = git.getClient(); + client.addDefaultCredentials(gitSCMSource.getCredentials()); + if (!client.hasGitRepo()) { + listener.getLogger().println("Creating git repository in " + cacheDir); + client.init(); + } + String remoteName = builder.remoteName(); + listener.getLogger().println("Setting " + remoteName + " to " + gitSCMSource.getRemote()); + client.setRemoteUrl(remoteName, gitSCMSource.getRemote()); + listener.getLogger().println("Fetching & pruning " + remoteName + "..."); + URIish remoteURI = null; + try { + remoteURI = new URIish(remoteName); + } catch (URISyntaxException ex) { + listener.getLogger().println("URI syntax exception for '" + remoteName + "' " + ex); + } + client.fetch_().prune(true).from(remoteURI, builder.asRefSpecs()).execute(); + listener.getLogger().println("Done."); + return new GitSCMFileSystem(client, gitSCMSource.getRemote(), Constants.R_REMOTES+remoteName+"/"+head.getName(), + (AbstractGitSCMSource.SCMRevisionImpl) rev); + } finally { + cacheLock.unlock(); + } + } + } +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMHeadMixin.java b/src/main/java/jenkins/plugins/git/GitSCMHeadMixin.java new file mode 100644 index 0000000000..30afbf7f34 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMHeadMixin.java @@ -0,0 +1,36 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package jenkins.plugins.git; + +import jenkins.scm.api.mixin.SCMHeadMixin; + +public interface GitSCMHeadMixin extends SCMHeadMixin { + + /** + * The ref, e.g. /refs/heads/master + * @return the ref + */ + String getRef(); // TODO provide a default implementation once Java 8 baseline +} \ No newline at end of file diff --git a/src/main/java/jenkins/plugins/git/GitSCMMatrixUtil.java b/src/main/java/jenkins/plugins/git/GitSCMMatrixUtil.java new file mode 100644 index 0000000000..7e80773b2e --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMMatrixUtil.java @@ -0,0 +1,32 @@ +package jenkins.plugins.git; + +import hudson.model.AbstractBuild; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.util.Collections; +import java.util.Set; + +/** + * Utility methods for integrating with Matrix Project plugin. + */ +@Restricted(NoExternalUse.class) +public class GitSCMMatrixUtil { + public static Set populateCandidatesFromRootBuild(AbstractBuild build, GitSCM scm) { + // every MatrixRun should build the same marked commit ID + AbstractBuild parentBuild = (build).getRootBuild(); + if (parentBuild != null) { + BuildData parentBuildData = scm.getBuildData(parentBuild); + if (parentBuildData != null) { + Build lastBuild = parentBuildData.lastBuild; + if (lastBuild != null) + return Collections.singleton(lastBuild.getMarked()); + } + } + return Collections.emptySet(); + } +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMSource.java b/src/main/java/jenkins/plugins/git/GitSCMSource.java index dcfdced0c7..86a25c6bb4 100644 --- a/src/main/java/jenkins/plugins/git/GitSCMSource.java +++ b/src/main/java/jenkins/plugins/git/GitSCMSource.java @@ -27,36 +27,88 @@ import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import hudson.RestrictedSince; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.model.ParameterValue; +import hudson.model.Queue; +import hudson.model.queue.Tasks; +import hudson.plugins.git.GitSCM; import hudson.plugins.git.GitStatus; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import hudson.scm.RepositoryBrowser; +import hudson.scm.SCM; import hudson.security.ACL; +import hudson.security.ACLContext; +import hudson.util.FormValidation; import hudson.util.ListBoxModel; +import java.io.ObjectStreamException; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; import jenkins.model.Jenkins; -import org.acegisecurity.Authentication; -import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; +import jenkins.plugins.git.traits.GitSCMExtensionTrait; +import jenkins.plugins.git.traits.GitSCMExtensionTraitDescriptor; +import jenkins.plugins.git.traits.GitToolSCMSourceTrait; +import jenkins.plugins.git.traits.IgnoreOnPushNotificationTrait; +import jenkins.plugins.git.traits.RefSpecsSCMSourceTrait; +import jenkins.plugins.git.traits.RemoteNameSCMSourceTrait; +import jenkins.scm.api.SCMEvent; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceDescriptor; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.api.trait.SCMTrait; +import jenkins.scm.impl.TagSCMHeadCategory; +import jenkins.scm.impl.UncategorizedSCMHeadCategory; +import jenkins.scm.impl.form.NamedArrayList; +import jenkins.scm.impl.trait.Discovery; +import jenkins.scm.impl.trait.Selection; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.Symbol; import org.jenkinsci.plugins.gitclient.GitClient; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import java.io.PrintWriter; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.logging.Logger; - /** - * @author Stephen Connolly + * A {@link SCMSource} that discovers branches in a git repository. */ public class GitSCMSource extends AbstractGitSCMSource { private static final String DEFAULT_INCLUDES = "*"; @@ -67,26 +119,242 @@ public class GitSCMSource extends AbstractGitSCMSource { private final String remote; - private final String credentialsId; + @CheckForNull + private String credentialsId; + + @Deprecated + private transient String remoteName; + + @Deprecated + private transient String rawRefSpecs; + + @Deprecated + private transient String includes; + + @Deprecated + private transient String excludes; + + @Deprecated + private transient boolean ignoreOnPushNotifications; - private final String includes; + @Deprecated + private transient GitRepositoryBrowser browser; - private final String excludes; + @Deprecated + private transient String gitTool; - private final boolean ignoreOnPushNotifications; + @Deprecated + private transient List extensions; + + /** + * Holds all the behavioural traits of this source. + * + * @since 3.4.0 + */ + private List traits = new ArrayList<>(); @DataBoundConstructor - public GitSCMSource(String id, String remote, String credentialsId, String includes, String excludes, boolean ignoreOnPushNotifications) { + public GitSCMSource(String remote) { + this.remote = remote; + } + + @DataBoundSetter + public void setCredentialsId(@CheckForNull String credentialsId) { + this.credentialsId = credentialsId; + } + + @DataBoundSetter + public void setTraits(List traits) { + this.traits = SCMTrait.asSetList(traits); + } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public GitSCMSource(String id, String remote, String credentialsId, String remoteName, String rawRefSpecs, String includes, String excludes, boolean ignoreOnPushNotifications) { super(id); this.remote = remote; this.credentialsId = credentialsId; - this.includes = includes; - this.excludes = excludes; - this.ignoreOnPushNotifications = ignoreOnPushNotifications; + List traits = new ArrayList<>(); + traits.add(new BranchDiscoveryTrait()); + if (!DEFAULT_INCLUDES.equals(includes) || !DEFAULT_EXCLUDES.equals(excludes)) { + traits.add(new WildcardSCMHeadFilterTrait(includes, excludes)); + } + if (!DEFAULT_REMOTE_NAME.equals(remoteName) && StringUtils.isNotBlank(remoteName)) { + traits.add(new RemoteNameSCMSourceTrait(remoteName)); + } + if (ignoreOnPushNotifications) { + traits.add(new IgnoreOnPushNotificationTrait()); + } + RefSpecsSCMSourceTrait trait = asRefSpecsSCMSourceTrait(rawRefSpecs, remoteName); + if (trait != null) { + traits.add(trait); + } + setTraits(traits); + } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public GitSCMSource(String id, String remote, String credentialsId, String includes, String excludes, boolean ignoreOnPushNotifications) { + this(id, remote, credentialsId, null, null, includes, excludes, ignoreOnPushNotifications); } + private Object readResolve() throws ObjectStreamException { + if (traits == null) { + List traits = new ArrayList<>(); + traits.add(new BranchDiscoveryTrait()); + if ((includes != null && !DEFAULT_INCLUDES.equals(includes)) + || (excludes != null && !DEFAULT_EXCLUDES.equals(excludes))) { + traits.add(new WildcardSCMHeadFilterTrait(includes, excludes)); + } + if (extensions != null) { + EXTENSIONS: + for (GitSCMExtension extension : extensions) { + for (SCMSourceTraitDescriptor d : SCMSourceTrait.all()) { + if (d instanceof GitSCMExtensionTraitDescriptor) { + GitSCMExtensionTraitDescriptor descriptor = (GitSCMExtensionTraitDescriptor) d; + if (descriptor.getExtensionClass().isInstance(extension)) { + try { + SCMSourceTrait trait = descriptor.convertToTrait(extension); + if (trait != null) { + traits.add(trait); + continue EXTENSIONS; + } + } catch (UnsupportedOperationException e) { + LOGGER.log(Level.WARNING, + "Could not convert " + extension.getClass().getName() + " to a trait", e); + } + } + } + LOGGER.log(Level.FINE, "Could not convert {0} to a trait (likely because this option does not " + + "make sense for a GitSCMSource)", getClass().getName()); + } + } + } + if (remoteName != null && !DEFAULT_REMOTE_NAME.equals(remoteName) && StringUtils.isNotBlank(remoteName)) { + traits.add(new RemoteNameSCMSourceTrait(remoteName)); + } + if (StringUtils.isNotBlank(gitTool)) { + traits.add(new GitToolSCMSourceTrait(gitTool)); + } + if (browser != null) { + traits.add(new GitBrowserSCMSourceTrait(browser)); + } + if (ignoreOnPushNotifications) { + traits.add(new IgnoreOnPushNotificationTrait()); + } + RefSpecsSCMSourceTrait trait = asRefSpecsSCMSourceTrait(rawRefSpecs, remoteName); + if (trait != null) { + traits.add(trait); + } + setTraits(traits); + } + return this; + } + + private RefSpecsSCMSourceTrait asRefSpecsSCMSourceTrait(String rawRefSpecs, String remoteName) { + if (rawRefSpecs != null) { + Set defaults = new HashSet<>(); + defaults.add("+refs/heads/*:refs/remotes/origin/*"); + if (remoteName != null) { + defaults.add("+refs/heads/*:refs/remotes/"+remoteName+"/*"); + } + if (!defaults.contains(rawRefSpecs.trim())) { + List templates = new ArrayList<>(); + for (String rawRefSpec : rawRefSpecs.split(" ")) { + if (StringUtils.isBlank(rawRefSpec)) { + continue; + } + if (defaults.contains(rawRefSpec)) { + templates.add(AbstractGitSCMSource.REF_SPEC_DEFAULT); + } else { + templates.add(rawRefSpec); + } + } + if (!templates.isEmpty()) { + return new RefSpecsSCMSourceTrait(templates.toArray(new String[0])); + } + } + } + return null; + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") public boolean isIgnoreOnPushNotifications() { - return ignoreOnPushNotifications; + return SCMTrait.find(traits, IgnoreOnPushNotificationTrait.class) != null; + } + + + // For Stapler only + @Restricted(DoNotUse.class) + @DataBoundSetter + public void setBrowser(GitRepositoryBrowser browser) { + List traits = new ArrayList<>(this.traits); + for (Iterator iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof GitBrowserSCMSourceTrait) { + iterator.remove(); + } + } + if (browser != null) { + traits.add(new GitBrowserSCMSourceTrait(browser)); + } + setTraits(traits); + } + + // For Stapler only + @Restricted(DoNotUse.class) + @DataBoundSetter + public void setGitTool(String gitTool) { + List traits = new ArrayList<>(this.traits); + gitTool = Util.fixEmptyAndTrim(gitTool); + for (Iterator iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof GitToolSCMSourceTrait) { + iterator.remove(); + } + } + if (gitTool != null) { + traits.add(new GitToolSCMSourceTrait(gitTool)); + } + setTraits(traits); + } + + // For Stapler only + @Restricted(DoNotUse.class) + @DataBoundSetter + @Deprecated + public void setExtensions(@CheckForNull List extensions) { + List traits = new ArrayList<>(this.traits); + for (Iterator iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof GitSCMExtensionTrait) { + iterator.remove(); + } + } + EXTENSIONS: + for (GitSCMExtension extension : Util.fixNull(extensions)) { + for (SCMSourceTraitDescriptor d : SCMSourceTrait.all()) { + if (d instanceof GitSCMExtensionTraitDescriptor) { + GitSCMExtensionTraitDescriptor descriptor = (GitSCMExtensionTraitDescriptor) d; + if (descriptor.getExtensionClass().isInstance(extension)) { + try { + SCMSourceTrait trait = descriptor.convertToTrait(extension); + if (trait != null) { + traits.add(trait); + continue EXTENSIONS; + } + } catch (UnsupportedOperationException e) { + LOGGER.log(Level.WARNING, + "Could not convert " + extension.getClass().getName() + " to a trait", e); + } + } + } + LOGGER.log(Level.FINE, "Could not convert {0} to a trait (likely because this option does not " + + "make sense for a GitSCMSource)", extension.getClass().getName()); + } + } + setTraits(traits); } @Override @@ -98,21 +366,58 @@ public String getRemote() { return remote; } - @Override - public String getIncludes() { - return includes; + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + public String getRawRefSpecs() { + String remoteName = null; + RefSpecsSCMSourceTrait refSpecs = null; + for (SCMSourceTrait trait : traits) { + if (trait instanceof RemoteNameSCMSourceTrait) { + remoteName = ((RemoteNameSCMSourceTrait) trait).getRemoteName(); + if (refSpecs != null) break; + } + if (trait instanceof RefSpecsSCMSourceTrait) { + refSpecs = (RefSpecsSCMSourceTrait) trait; + if (remoteName != null) break; + } + } + if (remoteName == null) { + remoteName = AbstractGitSCMSource.DEFAULT_REMOTE_NAME; + } + if (refSpecs == null) { + return AbstractGitSCMSource.REF_SPEC_DEFAULT + .replaceAll(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER, remoteName); + } + StringBuilder result = new StringBuilder(); + boolean first = true; + Pattern placeholder = Pattern.compile(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER); + for (String template : refSpecs.asStrings()) { + if (first) { + first = false; + } else { + result.append(' '); + } + result.append(placeholder.matcher(template).replaceAll(remoteName)); + } + return result.toString(); } + @Deprecated @Override - public String getExcludes() { - return excludes; + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + protected List getRefSpecs() { + return new GitSCMSourceContext<>(null, SCMHeadObserver.none()).withTraits(traits).asRefSpecs(); } + @NonNull @Override - protected List getRefSpecs() { - return Arrays.asList(new RefSpec("+refs/heads/*:refs/remotes/" + getRemoteName() + "/*")); + public List getTraits() { + return traits; } + @Symbol("git") @Extension public static class DescriptorImpl extends SCMSourceDescriptor { @@ -121,74 +426,260 @@ public String getDisplayName() { return Messages.GitSCMSource_DisplayName(); } - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, - @QueryParameter String remote) { - StandardListBoxModel result = new StandardListBoxModel(); - result.withEmptySelection(); - result.withMatching(GitClient.CREDENTIALS_MATCHER, - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context, + @QueryParameter String remote, + @QueryParameter String credentialsId) { + if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + context != null && !context.hasPermission(Item.EXTENDED_READ)) { + return new StandardListBoxModel().includeCurrentValue(credentialsId); + } + return new StandardListBoxModel() + .includeEmptyValue() + .includeMatchingAs( + context instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task)context) : ACL.SYSTEM, context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(remote).build() - ) - ); + StandardUsernameCredentials.class, + URIRequirementBuilder.fromUri(remote).build(), + GitClient.CREDENTIALS_MATCHER) + .includeCurrentValue(credentialsId); + } + + public FormValidation doCheckCredentialsId(@AncestorInPath Item context, + @QueryParameter String remote, + @QueryParameter String value) { + if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + context != null && !context.hasPermission(Item.EXTENDED_READ)) { + return FormValidation.ok(); + } + + value = Util.fixEmptyAndTrim(value); + if (value == null) { + return FormValidation.ok(); + } + + remote = Util.fixEmptyAndTrim(remote); + if (remote == null) + // not set, can't check + { + return FormValidation.ok(); + } + + for (ListBoxModel.Option o : CredentialsProvider.listCredentials( + StandardUsernameCredentials.class, + context, + context instanceof Queue.Task + ? Tasks.getAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + URIRequirementBuilder.fromUri(remote).build(), + GitClient.CREDENTIALS_MATCHER)) { + if (StringUtils.equals(value, o.value)) { + // TODO check if this type of credential is acceptable to the Git client or does it merit warning + // NOTE: we would need to actually lookup the credential to do the check, which may require + // fetching the actual credential instance from a remote credentials store. Perhaps this is + // not required + return FormValidation.ok(); + } + } + // no credentials available, can't check + return FormValidation.warning("Cannot find any credentials with id " + value); + } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("3.4.0") + public GitSCM.DescriptorImpl getSCMDescriptor() { + return (GitSCM.DescriptorImpl)Jenkins.getActiveInstance().getDescriptor(GitSCM.class); + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + public List getExtensionDescriptors() { + return getSCMDescriptor().getExtensionDescriptors(); + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + public List>> getBrowserDescriptors() { + return getSCMDescriptor().getBrowserDescriptors(); + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + public boolean showGitToolOptions() { + return getSCMDescriptor().showGitToolOptions(); + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("3.4.0") + public ListBoxModel doFillGitToolItems() { + return getSCMDescriptor().doFillGitToolItems(); + } + + public List> getTraitsDescriptorLists() { + List> result = new ArrayList<>(); + List descriptors = + SCMSourceTrait._for(this, GitSCMSourceContext.class, GitSCMBuilder.class); + NamedArrayList.select(descriptors, Messages.within_Repository(), + NamedArrayList.anyOf( + NamedArrayList.withAnnotation(Selection.class), + NamedArrayList.withAnnotation(Discovery.class) + ), + true, result); + NamedArrayList.select(descriptors, Messages.additional(), null, true, result); return result; } + public List getTraitsDefaults() { + return Collections.singletonList(new BranchDiscoveryTrait()); + } + @NonNull + @Override + protected SCMHeadCategory[] createCategories() { + return new SCMHeadCategory[]{UncategorizedSCMHeadCategory.DEFAULT, TagSCMHeadCategory.DEFAULT}; + } } @Extension public static class ListenerImpl extends GitStatus.Listener { - @Override - public List onNotifyCommit(URIish uri, String sha1, String... branches) { - List result = new ArrayList(); - boolean notified = false; + public List onNotifyCommit(String origin, + URIish uri, + @Nullable final String sha1, + List buildParameters, + String... branches) { + List result = new ArrayList<>(); + final boolean notified[] = {false}; // run in high privilege to see all the projects anonymous users don't see. // this is safe because when we actually schedule a build, it's a build that can // happen at some random time anyway. - SecurityContext old = Jenkins.getInstance().getACL().impersonate(ACL.SYSTEM); - try { - for (final SCMSourceOwner owner : SCMSourceOwners.all()) { - for (SCMSource source : owner.getSCMSources()) { - if (source instanceof GitSCMSource) { - GitSCMSource git = (GitSCMSource) source; - if (git.ignoreOnPushNotifications) { - continue; + try (ACLContext context = ACL.as(ACL.SYSTEM)) { + if (branches.length > 0) { + final URIish u = uri; + for (final String branch: branches) { + SCMHeadEvent.fireNow(new SCMHeadEvent(SCMEvent.Type.UPDATED, branch, origin){ + @Override + public boolean isMatch(@NonNull SCMNavigator navigator) { + return false; } - URIish remote; - try { - remote = new URIish(git.getRemote()); - } catch (URISyntaxException e) { - // ignore - continue; + + @NonNull + @Override + public String getSourceName() { + // we will never be called here as do not match any navigator + return u.getHumanishName(); } - if (GitStatus.looselyMatches(uri, remote)) { - LOGGER.info("Triggering the indexing of " + owner.getFullDisplayName()); - owner.onSCMSourceUpdated(source); - result.add(new GitStatus.ResponseContributor() { - @Override - public void addHeaders(StaplerRequest req, StaplerResponse rsp) { - rsp.addHeader("Triggered", owner.getAbsoluteUrl()); + + @Override + public boolean isMatch(SCMSource source) { + if (source instanceof GitSCMSource) { + GitSCMSource git = (GitSCMSource) source; + GitSCMSourceContext ctx = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()) + .withTraits(git.getTraits()); + if (ctx.ignoreOnPushNotifications()) { + return false; + } + URIish remote; + try { + remote = new URIish(git.getRemote()); + } catch (URISyntaxException e) { + // ignore + return false; + } + if (GitStatus.looselyMatches(u, remote)) { + notified[0] = true; + return true; } + return false; + } + return false; + } - @Override - public void writeBody(PrintWriter w) { - w.println("Scheduled indexing of " + owner.getFullDisplayName()); + @NonNull + @Override + public Map heads(@NonNull SCMSource source) { + if (source instanceof GitSCMSource) { + GitSCMSource git = (GitSCMSource) source; + GitSCMSourceContext ctx = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()) + .withTraits(git.getTraits()); + if (ctx.ignoreOnPushNotifications()) { + return Collections.emptyMap(); + } + URIish remote; + try { + remote = new URIish(git.getRemote()); + } catch (URISyntaxException e) { + // ignore + return Collections.emptyMap(); } - }); - notified = true; + if (GitStatus.looselyMatches(u, remote)) { + GitBranchSCMHead head = new GitBranchSCMHead(branch); + for (SCMHeadPrefilter filter: ctx.prefilters()) { + if (filter.isExcluded(git, head)) { + return Collections.emptyMap(); + } + } + return Collections.singletonMap(head, + sha1 != null ? new GitBranchSCMRevision(head, sha1) : null); + } + } + return Collections.emptyMap(); + } + + @Override + public boolean isMatch(@NonNull SCM scm) { + return false; // TODO rewrite the legacy event system to fire through SCM API + } + }); + } + } else { + for (final SCMSourceOwner owner : SCMSourceOwners.all()) { + for (SCMSource source : owner.getSCMSources()) { + if (source instanceof GitSCMSource) { + GitSCMSource git = (GitSCMSource) source; + GitSCMSourceContext ctx = + new GitSCMSourceContext<>(null, SCMHeadObserver.none()) + .withTraits(git.getTraits()); + if (ctx.ignoreOnPushNotifications()) { + continue; + } + URIish remote; + try { + remote = new URIish(git.getRemote()); + } catch (URISyntaxException e) { + // ignore + continue; + } + if (GitStatus.looselyMatches(uri, remote)) { + LOGGER.info("Triggering the indexing of " + owner.getFullDisplayName() + + " as a result of event from " + origin); + owner.onSCMSourceUpdated(source); + result.add(new GitStatus.ResponseContributor() { + @Override + public void addHeaders(StaplerRequest req, StaplerResponse rsp) { + rsp.addHeader("Triggered", owner.getAbsoluteUrl()); + } + + @Override + public void writeBody(PrintWriter w) { + w.println("Scheduled indexing of " + owner.getFullDisplayName()); + } + }); + notified[0] = true; + } } } } } - } finally { - SecurityContextHolder.setContext(old); } - if (!notified) { - result.add(new GitStatus.MessageResponseContributor("No git consumers for URI " + uri.toString())); + if (!notified[0]) { + result.add(new GitStatus.MessageResponseContributor("No Git consumers using SCM API plugin for: " + uri.toString())); } return result; } diff --git a/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java b/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java new file mode 100644 index 0000000000..494d027d2f --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java @@ -0,0 +1,455 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.TaskListener; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTool; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.transport.RefSpec; + +/** + * The {@link SCMSourceContext} for a {@link AbstractGitSCMSource}. + * + * @param the type of {@link GitSCMSourceContext} so that the {@link #withTrait(SCMSourceTrait)} etc methods can + * be chained easily by subclasses. + * @param the type of {@link GitSCMSourceRequest} produced by {@link #newRequest(SCMSource, TaskListener)}. + * @since 3.4.0 + */ +public class GitSCMSourceContext, R extends GitSCMSourceRequest> + extends SCMSourceContext { + /** + * {@code true} if the {@link GitSCMSourceRequest} will need information about branches. + */ + private boolean wantBranches; + /** + * {@code true} if the {@link GitSCMSourceRequest} will need information about tags. + */ + private boolean wantTags; + /** + * {@code true} if the {@link GitSCMSourceRequest} needs to be prune aware. + */ + private boolean pruneRefs; + /** + * A list of other references to discover and search + */ + private Set refNameMappings; + /** + * The name of the {@link GitTool} to use or {@code null} to use the default. + */ + @CheckForNull + private String gitTool; + /** + * Should push notifications be ignored. + */ + private boolean ignoreOnPushNotifications; + /** + * The ref specs to apply to the {@link GitSCM}. + */ + @NonNull + private List refSpecs = new ArrayList<>(); + /** + * The remote name. + */ + @NonNull + private String remoteName = AbstractGitSCMSource.DEFAULT_REMOTE_NAME; + + /** + * Constructor. + * + * @param criteria (optional) criteria. + * @param observer the {@link SCMHeadObserver}. + */ + public GitSCMSourceContext(@CheckForNull SCMSourceCriteria criteria, @NonNull SCMHeadObserver observer) { + super(criteria, observer); + } + + /** + * Returns {@code true} if the {@link GitSCMSourceRequest} will need information about branches. + * + * @return {@code true} if the {@link GitSCMSourceRequest} will need information about branches. + */ + public final boolean wantBranches() { + return wantBranches; + } + + /** + * Returns {@code true} if the {@link GitSCMSourceRequest} will need information about tags. + * + * @return {@code true} if the {@link GitSCMSourceRequest} will need information about tags. + */ + public final boolean wantTags() { + return wantTags; + } + + /** + * Returns {@code true} if the {@link GitSCMSourceRequest} needs to be prune aware. + * + * @return {@code true} if the {@link GitSCMSourceRequest} needs to be prune aware. + */ + public final boolean pruneRefs() { + return pruneRefs; + } + + /** + * Returns {@code true} if the {@link GitSCMSourceRequest} will need information about other refs. + * + * @return {@code true} if the {@link GitSCMSourceRequest} will need information about other refs. + */ + public final boolean wantOtherRefs() { + return refNameMappings != null && !refNameMappings.isEmpty(); + } + + @NonNull + public Collection getRefNameMappings() { + if (refNameMappings == null) { + return Collections.emptySet(); + } else { + return Collections.unmodifiableSet(refNameMappings); + } + } + + /** + * Returns the name of the {@link GitTool} to use or {@code null} to use the default. + * + * @return the name of the {@link GitTool} to use or {@code null} to use the default. + */ + @CheckForNull + public final String gitTool() { + return gitTool; + } + + /** + * Returns {@code true} if push notifications should be ignored. + * + * @return {@code true} if push notifications should be ignored. + */ + public final boolean ignoreOnPushNotifications() { + return ignoreOnPushNotifications; + } + + /** + * Returns the list of ref specs to use. + * + * @return the list of ref specs to use. + */ + @NonNull + public final List refSpecs() { + if (refSpecs.isEmpty()) { + return Collections.singletonList(AbstractGitSCMSource.REF_SPEC_DEFAULT); + } + return Collections.unmodifiableList(refSpecs); + } + + /** + * Returns the name to give the remote. + * + * @return the name to give the remote. + */ + @NonNull + public final String remoteName() { + return remoteName; + } + + /** + * Adds a requirement for branch details to any {@link GitSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public C wantBranches(boolean include) { + wantBranches = wantBranches || include; + return (C) this; + } + + /** + * Adds a requirement for tag details to any {@link GitSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public C wantTags(boolean include) { + wantTags = wantTags || include; + return (C) this; + } + + /** + * Adds a requirement for git ref pruning to any {@link GitSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public C pruneRefs(boolean include) { + pruneRefs = pruneRefs || include; + return (C) this; + } + + /** + * Adds a requirement for details of additional refs to any {@link GitSCMSourceRequest} for this context. + * + * @param other The specification for that other ref + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public C wantOtherRef(RefNameMapping other) { + if (refNameMappings == null) { + refNameMappings = new TreeSet<>(); + } + refNameMappings.add(other); + return (C) this; + } + + /** + * Configures the {@link GitTool#getName()} to use. + * + * @param gitTool the {@link GitTool#getName()} or {@code null} to use the system default. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withGitTool(String gitTool) { + this.gitTool = gitTool; + return (C) this; + } + + /** + * Configures whether push notifications should be ignored. + * + * @param ignoreOnPushNotifications {@code true} to ignore push notifications. + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withIgnoreOnPushNotifications(boolean ignoreOnPushNotifications) { + this.ignoreOnPushNotifications = ignoreOnPushNotifications; + return (C) this; + } + + /** + * Adds the specified ref spec. If no ref specs were previously defined then the supplied ref spec will replace + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT}. The ref spec is expected to be processed for substitution of + * {@link AbstractGitSCMSource#REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR} by {@link AbstractGitSCMSource#getRemote()} + * before use. + * + * @param refSpec the ref spec template to add. + * @return {@code this} for method chaining. + * @see #withoutRefSpecs() + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withRefSpec(@NonNull String refSpec) { + this.refSpecs.add(refSpec); + return (C) this; + } + + /** + * Adds the specified ref specs. If no ref specs were previously defined then the supplied ref specs will replace + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT}. The ref spec is expected to be processed for substitution of + * {@link AbstractGitSCMSource#REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR} by {@link AbstractGitSCMSource#getRemote()} + * before use. + * + * @param refSpecs the ref spec templates to add. + * @return {@code this} for method chaining. + * @see #withoutRefSpecs() + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withRefSpecs(List refSpecs) { + this.refSpecs.addAll(refSpecs); + return (C) this; + } + + /** + * Clears the specified ref specs. If no ref specs are subsequently defined then + * {@link AbstractGitSCMSource#REF_SPEC_DEFAULT} will be used as the ref spec template. + * + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withoutRefSpecs() { + this.refSpecs.clear(); + return (C) this; + } + + /** + * Configures the remote name to use for the git repository. + * + * @param remoteName the remote name to use for the git repository ({@code null} or the empty string are + * equivalent to passing {@link AbstractGitSCMSource#DEFAULT_REMOTE_NAME}). + * @return {@code this} for method chaining. + */ + @SuppressWarnings("unchecked") + @NonNull + public final C withRemoteName(String remoteName) { + this.remoteName = StringUtils.defaultIfBlank(remoteName, AbstractGitSCMSource.DEFAULT_REMOTE_NAME); + return (C) this; + } + + /** + * Converts the ref spec templates into {@link RefSpec} instances. + * + * @return the list of {@link RefSpec} instances. + */ + @NonNull + public final List asRefSpecs() { + List result = new ArrayList<>(Math.max(refSpecs.size(), 1)); + if (wantOtherRefs() && wantBranches()) { + //If wantOtherRefs() there will be a refspec in the list not added manually by a user + //So if also wantBranches() we need to add the default respec for branches so we actually fetch them + result.add(new RefSpec("+" + Constants.R_HEADS + "*:" + Constants.R_REMOTES + remoteName() + "/*")); + } + for (String template : refSpecs()) { + result.add(new RefSpec( + template.replaceAll(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER, remoteName()) + )); + } + return result; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @NonNull + @Override + public R newRequest(@NonNull SCMSource source, TaskListener listener) { + return (R) new GitSCMSourceRequest(source, this, listener); + } + + public static final class RefNameMapping implements Comparable { + private final String ref; + private final String name; + private transient Pattern refPattern; + + public RefNameMapping(@NonNull String ref, @NonNull String name) { + this.ref = ref; + this.name = name; + } + + @NonNull + public String getRef() { + return ref; + } + + @NonNull + public String getName() { + return name; + } + + Pattern refAsPattern() { + if (refPattern == null) { + refPattern = Pattern.compile(Constants.R_REFS + ref.replace("*", "(.+)")); + } + return refPattern; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RefNameMapping that = (RefNameMapping) o; + + return Objects.equals(ref, that.ref) + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(ref, name); + } + + @Override + public int compareTo(RefNameMapping o) { + return Integer.compare(this.hashCode(), o != null ? o.hashCode() : 0); + } + + public boolean matches(String revision, String remoteName, String remoteRev) { + final Matcher matcher = refAsPattern().matcher(remoteName); + if (matcher.matches()) { + //TODO support multiple capture groups? + if (matcher.groupCount() > 0) { //Group 0 apparently not in this count according to javadoc + String resolvedName = name.replace("@{1}", matcher.group(1)); + return resolvedName.equals(revision); + } else { + return name.equals(revision); + } + } + return false; + } + + public boolean matches(String remoteName) { + final Matcher matcher = refAsPattern().matcher(remoteName); + return matcher.matches(); + } + + public String getName(String remoteName) { + final Matcher matcher = refAsPattern().matcher(remoteName); + if (matcher.matches()) { + if (matcher.groupCount() > 0) { //Group 0 apparently not in this count according to javadoc + return name.replace("@{1}", matcher.group(1)); + } else if (!name.contains("@{1}")) { + return name; + } + } + return null; + } + } + +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMSourceDefaults.java b/src/main/java/jenkins/plugins/git/GitSCMSourceDefaults.java new file mode 100644 index 0000000000..fb38946e73 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMSourceDefaults.java @@ -0,0 +1,128 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import java.io.IOException; +import java.util.List; +import jenkins.scm.api.mixin.TagSCMHead; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteConfig; +import org.jenkinsci.plugins.gitclient.CloneCommand; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; + +/** + * Used to reset the default clone behaviour for {@link GitSCM} instances created by {@link GitSCMBuilder}. + * Does not have a descriptor as we do not expect this extension to be user-visible. + * With this extension, we anticipate: + *
    + *
  • tags will not be cloned or fetched
  • + *
  • refspecs will be honoured on clone
  • + *
+ * + * @since 3.4.0 + */ +public class GitSCMSourceDefaults extends GitSCMExtension { + + /** + * Determines whether tags should be fetched... only relevant if we implement support for {@link TagSCMHead}. + */ + private final boolean includeTags; + + /** + * Constructor. + * + * @param includeTags {@code true} to request fetching tags. + */ + public GitSCMSourceDefaults(boolean includeTags) { + this.includeTags = includeTags; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + GitSCMSourceDefaults that = (GitSCMSourceDefaults) o; + + return includeTags == that.includeTags; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (includeTags ? 1 : 0); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "GitSCMSourceDefaults{" + + "includeTags=" + includeTags + + '}'; + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateCloneCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, + CloneCommand cmd) throws IOException, InterruptedException, GitException { + listener.getLogger() + .printf("Cloning with configured refspecs honoured and %s tags%n", includeTags ? "with" : "without"); + RemoteConfig rc = scm.getRepositories().get(0); + List refspecs = rc.getFetchRefSpecs(); + cmd.refspecs(refspecs); + cmd.tags(includeTags); + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) + throws IOException, InterruptedException, GitException { + listener.getLogger() + .printf("Fetching %s tags%n", includeTags ? "with" : "without"); + cmd.tags(includeTags); + } +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMSourceRequest.java b/src/main/java/jenkins/plugins/git/GitSCMSourceRequest.java new file mode 100644 index 0000000000..7db56addb1 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMSourceRequest.java @@ -0,0 +1,111 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.TaskListener; +import hudson.plugins.git.GitTool; +import java.util.Collections; +import java.util.List; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMSourceRequest; +import org.eclipse.jgit.transport.RefSpec; + +/** + * The {@link SCMSourceRequest} base class for {@link AbstractGitSCMSource}. + * + * @since 3.4.0 + */ +public class GitSCMSourceRequest extends SCMSourceRequest { + /** + * {@code true} if branch details need to be fetched. + */ + private final boolean fetchBranches; + /** + * {@code true} if tag details need to be fetched. + */ + private final boolean fetchTags; + /** + * The {@link RefSpec} instances. + */ + private final List refSpecs; + /** + * The remote name. + */ + private final String remoteName; + /** + * The {@link GitTool#getName()}. + */ + private final String gitTool; + + /** + * Constructor. + * + * @param source the source. + * @param context the context. + * @param listener the (optional) {@link TaskListener}. + */ + public GitSCMSourceRequest(@NonNull SCMSource source, @NonNull GitSCMSourceContext context, TaskListener listener) { + super(source, context, listener); + fetchBranches = context.wantBranches(); + fetchTags = context.wantTags(); + remoteName = context.remoteName(); + gitTool = context.gitTool(); + refSpecs = Collections.unmodifiableList(context.asRefSpecs()); + } + + /** + * Returns the name of the {@link GitTool} to use or {@code null} to use the default. + * + * @return the name of the {@link GitTool} to use or {@code null} to use the default. + */ + @CheckForNull + public final String gitTool() { + return gitTool; + } + + /** + * Returns the name to give the remote. + * + * @return the name to give the remote. + */ + @NonNull + public final String remoteName() { + return remoteName; + } + + + /** + * Returns the list of {@link RefSpec} instances to use. + * + * @return the list of {@link RefSpec} instances to use. + */ + @NonNull + public final List refSpecs() { + return refSpecs; + } +} diff --git a/src/main/java/jenkins/plugins/git/GitSCMTelescope.java b/src/main/java/jenkins/plugins/git/GitSCMTelescope.java new file mode 100644 index 0000000000..4e762dfd97 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitSCMTelescope.java @@ -0,0 +1,387 @@ +/* + * The MIT License + * + * Copyright (c) 2017 Stephen Connolly + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionList; +import hudson.model.Item; +import hudson.model.Queue; +import hudson.model.queue.Tasks; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserRemoteConfig; +import hudson.scm.SCM; +import hudson.security.ACL; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.mixin.TagSCMHead; +import org.eclipse.jgit.lib.Constants; +import org.jenkinsci.plugins.gitclient.GitClient; + +/** + * An implementation of this extension point allows {@link AbstractGitSCMSource} to examine a repository from a distance + * without requiring a local checkout. + * + * @since 3.6.1 + */ +public abstract class GitSCMTelescope extends SCMFileSystem.Builder { + //TODO JENKINS-51134 DiscoverOtherRefsTrait + + /** + * Returns the {@link GitSCMTelescope} to use for the specified {@link GitSCM} or {@code null} if none match. + * @param source the {@link GitSCM}. + * @return the {@link GitSCMTelescope} to use for the specified {@link GitSCM} or {@code null} + */ + @CheckForNull + public static GitSCMTelescope of(@NonNull GitSCM source) { + for (SCMFileSystem.Builder b : ExtensionList.lookup(SCMFileSystem.Builder.class)) { + if (b instanceof GitSCMTelescope && b.supports(source)) { + return (GitSCMTelescope) b; + } + if (b instanceof GitSCMFileSystem.BuilderImpl) { + // telescopes must come before the fallback GitSCMFileSystem.BuilderImpl otherwise they would + // not prevent a local checkout + break; + } + } + return null; + } + + /** + * Returns the {@link GitSCMTelescope} to use for the specified {@link AbstractGitSCMSource} or {@code null} if + * none match. + * + * @param source the {@link AbstractGitSCMSource}. + * @return the {@link GitSCMTelescope} to use for the specified {@link AbstractGitSCMSource} or {@code null} + */ + @CheckForNull + public static GitSCMTelescope of(@NonNull AbstractGitSCMSource source) { + for (SCMFileSystem.Builder b : ExtensionList.lookup(SCMFileSystem.Builder.class)) { + if (b instanceof GitSCMTelescope && b.supports(source)) { + return (GitSCMTelescope) b; + } + if (GitSCMFileSystem.BuilderImpl.class.equals(b.getClass())) { + // telescopes must come before the fallback GitSCMFileSystem.BuilderImpl otherwise they would + // not prevent a local checkout + break; + } + } + return null; + } + + /** + * Checks if this {@link jenkins.scm.api.SCMFileSystem.Builder} supports the repository at the supplied remote URL. + * NOTE: returning {@code true} mandates that {@link #build(Item, SCM, SCMRevision)} and + * {@link #build(SCMSource, SCMHead, SCMRevision)} must return non-{@code null} when they are configured + * with the corresponding repository URL. + * + * @param remote the repository URL. + * @return {@code true} if and only if the remote URL is supported by this {@link GitSCMTelescope}. + */ + public abstract boolean supports(@NonNull String remote); + + /** + * Checks if the supplied credentials are valid against the specified repository URL. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} to validate anonymous connection. + * @throws IOException if the operation failed due to an IO error or invalid credentials. + * @throws InterruptedException if the operation was interrupted. + */ + public abstract void validate(@NonNull String remote, @CheckForNull StandardCredentials credentials) + throws IOException, InterruptedException; + + /** + * {@inheritDoc} + */ + @Override + public final boolean supports(@NonNull SCM source) { + if (source instanceof GitSCM) { + // we only support the GitSCM if the branch is completely unambiguous + GitSCM git = (GitSCM) source; + List configs = git.getUserRemoteConfigs(); + List branches = git.getBranches(); + if (configs.size() == 1) { + String remote = configs.get(0).getUrl(); + return remote != null + && supports(remote) + && branches.size() == 1 + && !branches.get(0).getName().contains("*"); + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean supports(@NonNull SCMSource source) { + return source instanceof AbstractGitSCMSource && source.getOwner() != null && supports( + ((AbstractGitSCMSource) source).getRemote()); + } + + /** + * {@inheritDoc} + */ + @Override + public final SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @CheckForNull SCMRevision rev) + throws IOException, InterruptedException { + SCMSourceOwner owner = source.getOwner(); + if (source instanceof AbstractGitSCMSource && owner != null && supports( + ((AbstractGitSCMSource) source).getRemote())) { + AbstractGitSCMSource git = (AbstractGitSCMSource) source; + String remote = git.getRemote(); + StandardUsernameCredentials credentials = git.getCredentials(); + validate(remote, credentials); + return build(remote, credentials, head, rev); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public final SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, SCMRevision rev) + throws IOException, InterruptedException { + if (scm instanceof GitSCM) { + // we only support the GitSCM if the branch is completely unambiguous + GitSCM git = (GitSCM) scm; + List configs = git.getUserRemoteConfigs(); + List branches = git.getBranches(); + if (configs.size() == 1) { + UserRemoteConfig config = configs.get(0); + String remote = config.getUrl(); + if (remote != null && supports(remote) + && branches.size() == 1 && !branches.get(0).getName().contains("*")) { + StandardCredentials credentials; + String credentialsId = config.getCredentialsId(); + if (credentialsId != null) { + List urlCredentials = CredentialsProvider + .lookupCredentials(StandardUsernameCredentials.class, owner, + owner instanceof Queue.Task + ? Tasks.getAuthenticationOf((Queue.Task) owner) + : ACL.SYSTEM, URIRequirementBuilder.fromUri(remote).build()); + credentials = CredentialsMatchers.firstOrNull( + urlCredentials, + CredentialsMatchers + .allOf(CredentialsMatchers.withId(credentialsId), GitClient.CREDENTIALS_MATCHER) + ); + } else { + credentials = null; + } + validate(remote, credentials); + SCMHead head; + if (rev == null) { + String name = branches.get(0).getName(); + if (name.startsWith(Constants.R_TAGS)) { + head = new GitTagSCMHead( + name.substring(Constants.R_TAGS.length()), + getTimestamp(remote, credentials, name) + ); + } else if (name.startsWith(Constants.R_HEADS)) { + head = new GitBranchSCMHead(name.substring(Constants.R_HEADS.length())); + } else { + if (name.startsWith(config.getName() + "/")) { + head = new GitBranchSCMHead(name.substring(config.getName().length() + 1)); + } else { + head = new GitBranchSCMHead(name); + } + } + } else { + head = rev.getHead(); + } + return build(remote, credentials, head, rev); + } + } + } + return null; + } + + /** + * Given a {@link SCM} this should try to build a corresponding {@link SCMFileSystem} instance that + * reflects the content at the specified {@link SCMRevision}. If the {@link SCM} is supported but not + * for a fixed revision, best effort is acceptable as the most capable {@link SCMFileSystem} will be returned + * to the caller. + * + * @param remote the repository URL + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param head the specified {@link SCMHead} + * @param rev the specified {@link SCMRevision}. + * @return the corresponding {@link SCMFileSystem} or {@code null} if this builder cannot create a {@link + * SCMFileSystem} for the specified repository URL. + * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error + * (such as the remote system being unavailable) + * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. + */ + @CheckForNull + protected abstract SCMFileSystem build(@NonNull String remote, @CheckForNull StandardCredentials credentials, + @NonNull SCMHead head, @CheckForNull SCMRevision rev) + throws IOException, InterruptedException; + + /** + * Retrieves the timestamp of the specified reference or object hash. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param refOrHash the reference or hash. If this is a reference then it will start with {@link Constants#R_REFS} + * If this is a hash, it may be a full hash or a short hash. + * @return the timestamp. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + public abstract long getTimestamp(@NonNull String remote, @CheckForNull StandardCredentials credentials, + @NonNull String refOrHash) throws IOException, InterruptedException; + + /** + * Retrieves the current revision of the specified reference or object hash. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param refOrHash the reference or hash. If this is a reference then it will start with {@link Constants#R_REFS} + * If this is a hash, it may be a full hash or a short hash. + * @return the revision or {@code null} if the reference or hash does not exist. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + @CheckForNull + public abstract SCMRevision getRevision(@NonNull String remote, + @CheckForNull StandardCredentials credentials, + @NonNull String refOrHash) + throws IOException, InterruptedException; + + /** + * Retrieves the timestamp of the specified reference or object hash. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param head the head. + * @return the timestamp. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + public long getTimestamp(@NonNull String remote, @CheckForNull StandardCredentials credentials, + @NonNull SCMHead head) throws IOException, InterruptedException { + if ((head instanceof TagSCMHead)) { + return getTimestamp(remote, credentials, Constants.R_TAGS + head.getName()); + } else { + return getTimestamp(remote, credentials, Constants.R_HEADS + head.getName()); + } + } + + /** + * Retrieves the current revision of the specified head. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param head the head. + * @return the revision or {@code null} if the head does not exist. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + @CheckForNull + public SCMRevision getRevision(@NonNull String remote, + @CheckForNull StandardCredentials credentials, + @NonNull SCMHead head) + throws IOException, InterruptedException { + if ((head instanceof TagSCMHead)) { + return getRevision(remote, credentials, Constants.R_TAGS + head.getName()); + } else { + return getRevision(remote, credentials, Constants.R_HEADS + head.getName()); + } + } + + /** + * Retrieves the current revisions of the specified repository. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @return the revisions. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + public final Iterable getRevisions(@NonNull String remote, + @CheckForNull StandardCredentials credentials) + throws IOException, InterruptedException { + return getRevisions(remote, credentials, EnumSet.allOf(ReferenceType.class)); + } + + /** + * Retrieves the current revisions of the specified repository. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @param referenceTypes the types of reference to retrieve revisions of. + * @return the revisions. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + public abstract Iterable getRevisions(@NonNull String remote, + @CheckForNull StandardCredentials credentials, + @NonNull Set referenceTypes) + throws IOException, InterruptedException; + + /** + * Retrieves the default target of the specified repository. + * + * @param remote the repository URL. + * @param credentials the credentials or {@code null} for an anonymous connection. + * @return the default target of the repository. + * @throws IOException if the operation failed due to an IO error. + * @throws InterruptedException if the operation was interrupted. + */ + public abstract String getDefaultTarget(@NonNull String remote, + @CheckForNull StandardCredentials credentials) + throws IOException, InterruptedException; + + /** + * The potential types of reference supported by a {@link GitSCMTelescope}. + */ + public enum ReferenceType { + /** + * A regular reference. + */ + HEAD, + /** + * A tag reference. + */ + TAG; + } +} diff --git a/src/main/java/jenkins/plugins/git/GitStep.java b/src/main/java/jenkins/plugins/git/GitStep.java new file mode 100644 index 0000000000..99a4f6f80a --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitStep.java @@ -0,0 +1,121 @@ +/* + * The MIT License + * + * Copyright 2014 Jesse Glick. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.plugins.git; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.Util; +import hudson.model.Item; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.scm.SCM; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.Collections; +import org.jenkinsci.plugins.workflow.steps.scm.SCMStep; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +/** + * Runs Git using {@link GitSCM}. + */ +public final class GitStep extends SCMStep { + + private final String url; + private String branch = "master"; + private String credentialsId; + + @DataBoundConstructor + public GitStep(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public String getBranch() { + return branch; + } + + public String getCredentialsId() { + return credentialsId; + } + + @DataBoundSetter + public void setBranch(String branch) { + this.branch = branch; + } + + @DataBoundSetter + public void setCredentialsId(String credentialsId) { + this.credentialsId = Util.fixEmpty(credentialsId); + } + + @Override + public SCM createSCM() { + return new GitSCM(GitSCM.createRepoList(url, credentialsId), Collections.singletonList(new BranchSpec("*/" + branch)), false, Collections.emptyList(), null, null, Collections.singletonList(new LocalBranch(branch))); + } + + @Extension + public static final class DescriptorImpl extends SCMStepDescriptor { + + @Inject + private UserRemoteConfig.DescriptorImpl delegate; + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item project, + @QueryParameter String url, + @QueryParameter String credentialsId) { + return delegate.doFillCredentialsIdItems(project, url, credentialsId); + } + + @RequirePOST + public FormValidation doCheckUrl(@AncestorInPath Item item, + @QueryParameter String credentialsId, + @QueryParameter String value) throws IOException, InterruptedException { + return delegate.doCheckUrl(item, credentialsId, value); + } + + @Override + public String getFunctionName() { + return "git"; + } + + @Override + public String getDisplayName() { + return Messages.GitStep_git(); + } + + } + +} diff --git a/src/main/java/jenkins/plugins/git/GitTagSCMHead.java b/src/main/java/jenkins/plugins/git/GitTagSCMHead.java new file mode 100644 index 0000000000..07512cd636 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitTagSCMHead.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.mixin.TagSCMHead; + +/** + * Represents a Git Tag. + * + * @since 3.6.0 + */ +public class GitTagSCMHead extends SCMHead implements TagSCMHead { + /** + * The timestamp of the tag, for lightweight tags this should be the last commit, for annotated + * tags this should be the tag date. + */ + private final long timestamp; + + /** + * Constructor. + * + * @param name the name. + * @param timestamp the timestamp of the tag, for lightweight tags this should be the last commit, for annotated + * tags this should be the tag date. + */ + public GitTagSCMHead(@NonNull String name, long timestamp) { + super(name); + this.timestamp = timestamp; + } + + /** + * {@inheritDoc} + */ + @Override + public long getTimestamp() { + return timestamp; + } + +} diff --git a/src/main/java/jenkins/plugins/git/GitTagSCMRevision.java b/src/main/java/jenkins/plugins/git/GitTagSCMRevision.java new file mode 100644 index 0000000000..4271539321 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitTagSCMRevision.java @@ -0,0 +1,43 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Represents the revision of a Git Tag. + * + * @since 3.6.0 + */ +public class GitTagSCMRevision extends AbstractGitSCMSource.SCMRevisionImpl { + /** + * Constructor. + * + * @param head the head. + * @param hash the revision hash. + */ + public GitTagSCMRevision(@NonNull GitTagSCMHead head, @NonNull String hash) { + super(head, hash); + } +} diff --git a/src/main/java/jenkins/plugins/git/MatrixGitPublisher.java b/src/main/java/jenkins/plugins/git/MatrixGitPublisher.java new file mode 100644 index 0000000000..4e23b0033d --- /dev/null +++ b/src/main/java/jenkins/plugins/git/MatrixGitPublisher.java @@ -0,0 +1,30 @@ +package jenkins.plugins.git; + +import hudson.Extension; +import hudson.Launcher; +import hudson.matrix.MatrixAggregatable; +import hudson.matrix.MatrixAggregator; +import hudson.matrix.MatrixBuild; +import hudson.model.BuildListener; +import hudson.plugins.git.GitPublisher; + +import java.io.IOException; + +@Extension(optional = true) +public class MatrixGitPublisher implements MatrixAggregatable { + /** + * For a matrix project, push should only happen once. + */ + public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { + return new MatrixAggregator(build,launcher,listener) { + @Override + public boolean endBuild() throws InterruptedException, IOException { + GitPublisher publisher = build.getParent().getPublishersList().get(GitPublisher.class); + if (publisher != null) { + return publisher.perform(build, launcher, listener); + } + return true; + } + }; + } +} diff --git a/src/main/java/jenkins/plugins/git/MergeWithGitSCMExtension.java b/src/main/java/jenkins/plugins/git/MergeWithGitSCMExtension.java new file mode 100644 index 0000000000..80f52a5ebd --- /dev/null +++ b/src/main/java/jenkins/plugins/git/MergeWithGitSCMExtension.java @@ -0,0 +1,146 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.PreBuildMerge; +import hudson.plugins.git.util.MergeRecord; +import java.io.IOException; +import jenkins.scm.api.SCMSource; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.CloneCommand; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.MergeCommand; + +/** + * Similar to {@link PreBuildMerge}, but for use from {@link SCMSource} implementations that need to specify the exact + * base branch hash. The hash is specified so that we are not subject to a race condition between the {@code baseHash} + * we think we are merging with and a possibly newer one that was just pushed. + *

+ * IMPORTANT This extension is intended for programmatic use only. It must be the last extension + * in the list of extensions or else some other extension may turn on shallow cloning. + * + * @since 3.5.0 + */ +public class MergeWithGitSCMExtension extends GitSCMExtension { + @NonNull + private final String baseName; + @CheckForNull + private final String baseHash; + + public MergeWithGitSCMExtension(@NonNull String baseName, @CheckForNull String baseHash) { + this.baseName = baseName; + this.baseHash = baseHash; + } + + @NonNull + public String getBaseName() { + return baseName; + } + + public String getBaseHash() { + return baseHash; + } + + @Override + public void decorateCloneCommand(GitSCM scm, Run build, GitClient git, TaskListener listener, + CloneCommand cmd) throws IOException, InterruptedException, GitException { + // we are doing a merge, so cannot permit a shallow clone + cmd.shallow(false); + } + + @Override + public void decorateFetchCommand(GitSCM scm, GitClient git, TaskListener listener, FetchCommand cmd) + throws IOException, InterruptedException, GitException { + // we are doing a merge, so cannot permit a shallow clone + cmd.shallow(false); + } + + @Override + public Revision decorateRevisionToBuild(GitSCM scm, Run build, GitClient git, TaskListener listener, + Revision marked, Revision rev) + throws IOException, InterruptedException, GitException { + ObjectId baseObjectId; + if (StringUtils.isBlank(baseHash)) { + try { + baseObjectId = git.revParse(Constants.R_REFS + baseName); + } catch (GitException e) { + listener.getLogger().printf("Unable to determine head revision of %s prior to merge with PR%n", + baseName); + throw e; + } + } else { + baseObjectId = ObjectId.fromString(baseHash); + } + listener.getLogger().printf("Merging %s commit %s into PR head commit %s%n", + baseName, baseObjectId.name(), rev.getSha1String() + ); + checkout(scm, build, git, listener, rev); + try { + /* could parse out of JenkinsLocationConfiguration.get().getAdminAddress() but seems overkill */ + git.setAuthor("Jenkins", "nobody@nowhere"); + git.setCommitter("Jenkins", "nobody@nowhere"); + MergeCommand cmd = git.merge().setRevisionToMerge(baseObjectId); + for (GitSCMExtension ext : scm.getExtensions()) { + // By default we do a regular merge, allowing it to fast-forward. + ext.decorateMergeCommand(scm, build, git, listener, cmd); + } + cmd.execute(); + } catch (GitException x) { + // TODO clarify these TODO comments copied from GitHub Branch Source + + // Try to revert merge conflict markers. + // TODO IGitAPI offers a reset(hard) method yet GitClient does not. Why? + checkout(scm, build, git, listener, rev); + // TODO would be nicer to throw an AbortException with just the message, but this is actually worse + // until git-client 1.19.7+ + throw x; + } + build.addAction( + new MergeRecord(baseName, baseObjectId.getName())); // does not seem to be used, but just in case + ObjectId mergeRev = git.revParse(Constants.HEAD); + listener.getLogger().println("Merge succeeded, producing " + mergeRev.name()); + return new Revision(mergeRev, rev.getBranches()); // note that this ensures Build.revision != Build.marked + } + + private void checkout(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision rev) + throws InterruptedException, IOException, GitException { + CheckoutCommand checkoutCommand = git.checkout().ref(rev.getSha1String()); + for (GitSCMExtension ext : scm.getExtensions()) { + ext.decorateCheckoutCommand(scm, build, git, listener, checkoutCommand); + } + checkoutCommand.execute(); + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/AuthorInChangelogTrait.java b/src/main/java/jenkins/plugins/git/traits/AuthorInChangelogTrait.java new file mode 100644 index 0000000000..c9528697d1 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/AuthorInChangelogTrait.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.AuthorInChangelog; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link AuthorInChangelog} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class AuthorInChangelogTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + */ + @DataBoundConstructor + public AuthorInChangelogTrait() { + super(new AuthorInChangelog()); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Use commit author in changelog"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/BranchDiscoveryTrait.java b/src/main/java/jenkins/plugins/git/traits/BranchDiscoveryTrait.java new file mode 100644 index 0000000000..3dbb845dd5 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/BranchDiscoveryTrait.java @@ -0,0 +1,163 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.SCMHeadMixin; +import jenkins.scm.api.mixin.TagSCMHead; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMHeadAuthority; +import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.trait.Discovery; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link Discovery} trait for Git that will discover branches on the repository. + * + * @since 3.4.0 + */ +public class BranchDiscoveryTrait extends SCMSourceTrait { + /** + * Constructor for stapler. + */ + @DataBoundConstructor + public BranchDiscoveryTrait() { + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + GitSCMSourceContext ctx = (GitSCMSourceContext) context; + ctx.wantBranches(true); + ctx.withAuthority(new BranchSCMHeadAuthority()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean includeCategory(@NonNull SCMHeadCategory category) { + return category.isUncategorized(); + } + + /** + * BranchDiscoveryTrait descriptor. + */ + @Symbol("gitBranchDiscovery") + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.BranchDiscoveryTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return GitSCMSource.class; + } + } + + /** + * Trusts branches from the repository. + */ + public static class BranchSCMHeadAuthority extends SCMHeadAuthority { + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull SCMHead head) { + return true; + } + + /** + * Out descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.BranchDiscoveryTrait_authorityDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Default.class.isAssignableFrom(originClass); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToHead(@NonNull Class headClass) { + return super.isApplicableToHead(headClass) && !(TagSCMHead.class.isAssignableFrom(headClass)); + } + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/CheckoutOptionTrait.java b/src/main/java/jenkins/plugins/git/traits/CheckoutOptionTrait.java new file mode 100644 index 0000000000..241798c92e --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/CheckoutOptionTrait.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.CheckoutOption; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link CheckoutOption} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class CheckoutOptionTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + * + * @param extension the {@link CheckoutOption} + */ + @DataBoundConstructor + public CheckoutOptionTrait(CheckoutOption extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Advanced checkout behaviours"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/CleanAfterCheckoutTrait.java b/src/main/java/jenkins/plugins/git/traits/CleanAfterCheckoutTrait.java new file mode 100644 index 0000000000..ac755b570c --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/CleanAfterCheckoutTrait.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.CleanCheckout; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * Exposes {@link CleanCheckout} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class CleanAfterCheckoutTrait extends GitSCMExtensionTrait { + + /** + * @deprecated Use constructor that accepts extension instead. + */ + @Deprecated + public CleanAfterCheckoutTrait() { + this(null); + } + + /** + * Stapler constructor. + * + * @param extension the option to clean subdirectories which contain git repositories. + */ + @DataBoundConstructor + public CleanAfterCheckoutTrait(@CheckForNull CleanCheckout extension) { + super(extension == null ? new CleanCheckout() : extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Clean after checkout"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/CleanBeforeCheckoutTrait.java b/src/main/java/jenkins/plugins/git/traits/CleanBeforeCheckoutTrait.java new file mode 100644 index 0000000000..42e114007d --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/CleanBeforeCheckoutTrait.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.CleanBeforeCheckout; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * Exposes {@link CleanBeforeCheckout} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class CleanBeforeCheckoutTrait extends GitSCMExtensionTrait { + + /** + * @deprecated Use constructor that accepts extension instead. + */ + @Deprecated + public CleanBeforeCheckoutTrait() { + this(null); + } + + /** + * Stapler constructor. + * + * @param extension the option to clean subdirectories which contain git repositories. + */ + @DataBoundConstructor + public CleanBeforeCheckoutTrait(@CheckForNull CleanBeforeCheckout extension) { + super(extension == null ? new CleanBeforeCheckout() : extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Clean before checkout"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/CloneOptionTrait.java b/src/main/java/jenkins/plugins/git/traits/CloneOptionTrait.java new file mode 100644 index 0000000000..3e91fdaedf --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/CloneOptionTrait.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.CloneOption; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link CloneOption} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class CloneOptionTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + * + * @param extension the {@link CloneOption} + */ + @DataBoundConstructor + public CloneOptionTrait(CloneOption extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.Advanced_clone_behaviours(); + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/DiscoverOtherRefsTrait.java b/src/main/java/jenkins/plugins/git/traits/DiscoverOtherRefsTrait.java new file mode 100644 index 0000000000..8d43eec1be --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/DiscoverOtherRefsTrait.java @@ -0,0 +1,154 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.trait.Discovery; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import static jenkins.plugins.git.AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR; + +public class DiscoverOtherRefsTrait extends SCMSourceTrait { + + private final String ref; + private String nameMapping; + + @DataBoundConstructor + public DiscoverOtherRefsTrait(String ref) { + if (StringUtils.isEmpty(ref)) { + throw new IllegalArgumentException("ref can not be empty"); + } + this.ref = StringUtils.removeStart(StringUtils.removeStart(ref, Constants.R_REFS), "/"); + setDefaultNameMapping(); + } + + //for easier testing + public DiscoverOtherRefsTrait(String ref, String nameMapping) { + this(ref); + setNameMapping(nameMapping); + } + + public String getRef() { + return ref; + } + + String getFullRefSpec() { + return new StringBuilder("+") + .append(Constants.R_REFS).append(ref) + .append(':').append(Constants.R_REMOTES) + .append(REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR) + .append('/').append(ref).toString(); + } + + public String getNameMapping() { + return nameMapping; + } + + @DataBoundSetter + public void setNameMapping(String nameMapping) { + if (StringUtils.isEmpty(nameMapping)) { + setDefaultNameMapping(); + } else { + this.nameMapping = nameMapping; + } + } + + private void setDefaultNameMapping() { + this.nameMapping = null; + String[] paths = ref.split("/"); + for (int i = 0; i < paths.length; i++) { + if("*".equals(paths[i]) && i > 0) { + this.nameMapping = paths[i-1] + "-@{1}"; + break; + } + } + if (StringUtils.isEmpty(this.nameMapping)) { + if (ref.contains("*")) { + this.nameMapping = "other-@{1}"; + } else { + this.nameMapping = "other-ref"; + } + } + } + + @Override + protected void decorateContext(SCMSourceContext context) { + GitSCMSourceContext c = (GitSCMSourceContext) context; + c.withRefSpec(getFullRefSpec()); + c.wantOtherRef(new GitSCMSourceContext.RefNameMapping(this.ref, this.nameMapping)); + } + + /** + * Our descriptor. + */ + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String getDisplayName() { + return Messages.DiscoverOtherRefsTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return GitSCMSource.class; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait.java b/src/main/java/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait.java new file mode 100644 index 0000000000..354d7d4fae --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait.java @@ -0,0 +1,151 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.scm.RepositoryBrowser; +import hudson.scm.SCM; +import java.util.List; +import jenkins.model.Jenkins; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link GitRepositoryBrowser} configuration of a {@link AbstractGitSCMSource} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class GitBrowserSCMSourceTrait extends SCMSourceTrait { + + /** + * The configured {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + */ + @CheckForNull + private final GitRepositoryBrowser browser; + + /** + * Stapler constructor. + * + * @param browser the {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + */ + @DataBoundConstructor + public GitBrowserSCMSourceTrait(@CheckForNull GitRepositoryBrowser browser) { + this.browser = browser; + } + + /** + * Gets the {@link GitRepositoryBrowser}.. + * + * @return the {@link GitRepositoryBrowser} or {@code null} to use the "auto" browser. + */ + @CheckForNull + public GitRepositoryBrowser getBrowser() { + return browser; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + ((GitSCMBuilder) builder).withBrowser(browser); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Configure Repository Browser"; + } + + /** + * Expose the {@link GitRepositoryBrowser} instances to stapler. + * + * @return the {@link GitRepositoryBrowser} instances + */ + @Restricted(NoExternalUse.class) // stapler + public List>> getBrowserDescriptors() { + GitSCM.DescriptorImpl descriptor = (GitSCM.DescriptorImpl) Jenkins.get().getDescriptor(GitSCM.class); + if (descriptor == null) { + return java.util.Collections.emptyList(); // Should be unreachable + } + return descriptor.getBrowserDescriptors(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return GitSCMSource.class; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/GitLFSPullTrait.java b/src/main/java/jenkins/plugins/git/traits/GitLFSPullTrait.java new file mode 100644 index 0000000000..4ad438a9f9 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/GitLFSPullTrait.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.GitLFSPull; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link GitLFSPull} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class GitLFSPullTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + */ + @DataBoundConstructor + public GitLFSPullTrait() { + super(new GitLFSPull()); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Git LFS pull after checkout"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTrait.java b/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTrait.java new file mode 100644 index 0000000000..69f85f081f --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTrait.java @@ -0,0 +1,73 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.git.extensions.GitSCMExtension; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceTrait; + +/** + * Base class for exposing a {@link GitSCMExtension} as a {@link SCMSourceTrait}. + * + * @param the {@link GitSCMExtension} that is being exposed + * @since 3.4.0 + */ +public abstract class GitSCMExtensionTrait extends SCMSourceTrait { + /** + * The extension. + */ + @NonNull + private final E extension; + + /** + * Constructor. + * + * @param extension the extension. + */ + public GitSCMExtensionTrait(@NonNull E extension) { + this.extension = extension; + } + + /** + * Gets the extension. + * + * @return the extension. + */ + @NonNull + public E getExtension() { + return extension; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + ((GitSCMBuilder) builder).withExtension(extension); + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTraitDescriptor.java b/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTraitDescriptor.java new file mode 100644 index 0000000000..a4a735382b --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/GitSCMExtensionTraitDescriptor.java @@ -0,0 +1,245 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.scm.SCM; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import javax.annotation.CheckForNull; +import jenkins.model.Jenkins; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.api.trait.SCMTrait; +import org.jvnet.tiger_types.Types; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Base class for the {@link Descriptor} of a {@link GitSCMExtension}. + * + * @since 3.4.0 + */ +public abstract class GitSCMExtensionTraitDescriptor extends SCMSourceTraitDescriptor { + + /** + * The type of {@link GitSCMExtension}. + */ + @NonNull + private final Class extension; + /** + * The constructor to use in {@link #convertToTrait(GitSCMExtension)} or {@code null} if the implementation + * class is handling conversion. + */ + @CheckForNull + private final Constructor constructor; + /** + * {@code true} if {@link #constructor} does not take any parameters, {@code false} if it takes a single parameter + * of type {@link GitSCMExtension}. + */ + private final boolean noArgConstructor; + + /** + * Constructor to use when type inference using {@link #GitSCMExtensionTraitDescriptor()} does not work. + * + * @param clazz Pass in the type of {@link SCMTrait} + * @param extension Pass in the type of {@link GitSCMExtension}. + */ + protected GitSCMExtensionTraitDescriptor(Class clazz, + Class extension) { + super(clazz); + this.extension = extension; + if (!Util.isOverridden(GitSCMExtensionTraitDescriptor.class, getClass(), "convertToTrait", + GitSCMExtension.class)) { + // check that the GitSCMExtensionTrait has a constructor that takes a single argument of the type + // 'extension' so that our default convertToTrait method implementation can be used + try { + constructor = clazz.getConstructor(extension); + noArgConstructor = constructor.getParameterTypes().length == 0; + } catch (NoSuchMethodException e) { + throw new AssertionError("Could not infer how to convert a " + extension + " to a " + + clazz + " as there is no obvious constructor. Either provide a simple constructor or " + + "override convertToTrait(GitSCMExtension)", e); + } + } else { + constructor = null; + noArgConstructor = false; + } + } + + /** + * Infers the type of the corresponding {@link GitSCMExtensionTrait} from the outer class. + * This version works when you follow the common convention, where a descriptor + * is written as the static nested class of the describable class. + */ + protected GitSCMExtensionTraitDescriptor() { + super(); + Type bt = Types.getBaseClass(clazz, GitSCMExtensionTrait.class); + if (bt instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) bt; + // this 'extension' is the closest approximation of E of GitSCMExtensionTrait. + extension = Types.erasure(pt.getActualTypeArguments()[0]); + if (!GitSCMExtension.class.isAssignableFrom(extension) || GitSCMExtension.class == extension) { + throw new AssertionError("Could not infer GitSCMExtension type for outer class " + clazz + + " of " + getClass() + ". Perhaps wrong outer class? (or consider using the explicit " + + "class constructor)"); + } + } else { + throw new AssertionError("Could not infer GitSCMExtension type. Consider using the explicit " + + "class constructor)"); + } + if (!Util.isOverridden(GitSCMExtensionTraitDescriptor.class, getClass(), "convertToTrait", + GitSCMExtension.class)) { + // check that the GitSCMExtensionTrait has a constructor that takes a single argument of the type + // 'extension' so that our default convertToTrait method implementation can be used + Constructor constructor = null; + for (Constructor c : clazz.getConstructors()) { + if (c.getAnnotation(DataBoundConstructor.class) != null) { + constructor = (Constructor) c; + break; + } + } + if (constructor != null) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + this.constructor = constructor; + this.noArgConstructor = true; + } else if (parameterTypes.length == 1 && extension.equals(parameterTypes[0])) { + this.constructor = constructor; + this.noArgConstructor = false; + } else { + throw new AssertionError("Could not infer how to convert a " + extension + " to a " + + clazz + " as the @DataBoundConstructor is neither zero arg nor single arg of type " + + extension + ". Either provide a simple constructor or override " + + "convertToTrait(GitSCMExtension)"); + } + } else { + throw new AssertionError("Could not infer how to convert a " + extension + " to a " + + clazz + " as there is no @DataBoundConstructor (which is going to cause other problems)"); + } + } else { + constructor = null; + this.noArgConstructor = false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * Returns the {@link GitSCMExtensionDescriptor} for this {@link #getExtensionClass()}. + * + * @return the {@link GitSCMExtensionDescriptor} for this {@link #getExtensionClass()}. + */ + @Restricted(NoExternalUse.class) // intended for use from stapler / jelly only + public GitSCMExtensionDescriptor getExtensionDescriptor() { + return (GitSCMExtensionDescriptor) Jenkins.get().getDescriptor(extension); + } + + /** + * Returns the type of {@link GitSCMExtension} that the {@link GitSCMExtensionTrait} wraps. + * + * @return the type of {@link GitSCMExtension} that the {@link GitSCMExtensionTrait} wraps. + */ + public Class getExtensionClass() { + return extension; + } + + /** + * Converts the supplied {@link GitSCMExtension} (which must be of type {@link #getExtensionClass()}) into + * its corresponding {@link GitSCMExtensionTrait}. + * + * The default implementation assumes that the {@link #clazz} has a public constructor taking either no arguments + * or a single argument of type {@link #getExtensionClass()} and will just call that. Override this method if you + * need more complex conversion logic, for example {@link LocalBranch} only makes sense for a + * {@link LocalBranch#getLocalBranch()} value of {@code **} so + * {@link LocalBranchTrait.DescriptorImpl#convertToTrait(GitSCMExtension)} returns {@code null} for all other + * {@link LocalBranch} configurations. + * + * @param extension the {@link GitSCMExtension} (must be of type {@link #getExtensionClass()}) + * @return the {@link GitSCMExtensionTrait} or {@code null} if the supplied {@link GitSCMExtension} is not + * appropriate for conversion to a {@link GitSCMExtensionTrait} + * @throws UnsupportedOperationException if the conversion failed because of a implementation bug. + */ + @CheckForNull + public SCMSourceTrait convertToTrait(@NonNull GitSCMExtension extension) { + if (!this.extension.isInstance(extension)) { + throw new IllegalArgumentException( + "Expected a " + this.extension.getName() + " but got a " + extension.getClass().getName() + ); + } + if (constructor == null) { + if (!Util.isOverridden(GitSCMExtensionTraitDescriptor.class, getClass(), "convertToTrait", + GitSCMExtension.class)) { + throw new IllegalStateException("Should not be able to instantiate a " + getClass().getName() + + " without an inferred constructor for " + this.extension.getName()); + } + throw new UnsupportedOperationException( + getClass().getName() + " should not delegate convertToTrait() to " + GitSCMExtension.class + .getName()); + } + try { + return noArgConstructor + ? constructor.newInstance() + : constructor.newInstance(this.extension.cast(extension)); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | ClassCastException e) { + throw new UnsupportedOperationException(e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getHelpFile() { + String primary = super.getHelpFile(); + return primary == null ? getExtensionDescriptor().getHelpFile() : primary; + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/GitToolSCMSourceTrait.java b/src/main/java/jenkins/plugins/git/traits/GitToolSCMSourceTrait.java new file mode 100644 index 0000000000..dccbe86e6b --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/GitToolSCMSourceTrait.java @@ -0,0 +1,170 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Util; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTool; +import hudson.scm.SCM; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link GitTool} configuration of a {@link AbstractGitSCMSource} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class GitToolSCMSourceTrait extends SCMSourceTrait { + /** + * The {@link GitTool#getName()} or {@code null} to use the "system" default. + */ + @CheckForNull + private final String gitTool; + + /** + * Stapler constructor. + * + * @param gitTool the {@link GitTool#getName()} or {@code null} to use the "system" default. + */ + @DataBoundConstructor + public GitToolSCMSourceTrait(@CheckForNull String gitTool) { + this.gitTool = Util.fixEmpty(gitTool); + } + + /** + * Returns the {@link GitTool#getName()}. + * + * @return the {@link GitTool#getName()} or {@code null} to use the "system" default. + */ + @CheckForNull + public String getGitTool() { + return gitTool; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + ((GitSCMSourceContext)context).withGitTool(gitTool); + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + ((GitSCMBuilder) builder).withGitTool(gitTool); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Select Git executable"; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToBuilder(@NonNull Class builderClass) { + return super.isApplicableToBuilder(builderClass) && getSCMDescriptor().showGitToolOptions(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return super.isApplicableToContext(contextClass) && getSCMDescriptor().showGitToolOptions(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * Gets the {@link GitSCM.DescriptorImpl} so that we can delegate some decisions to it. + * + * @return the {@link GitSCM.DescriptorImpl}. + */ + private GitSCM.DescriptorImpl getSCMDescriptor() { + return (GitSCM.DescriptorImpl) Jenkins.get().getDescriptor(GitSCM.class); + } + + /** + * Returns the list of {@link GitTool} items. + * + * @return the list of {@link GitTool} items. + */ + @Restricted(NoExternalUse.class) // stapler + public ListBoxModel doFillGitToolItems() { + return getSCMDescriptor().doFillGitToolItems(); + } + + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/IgnoreOnPushNotificationTrait.java b/src/main/java/jenkins/plugins/git/traits/IgnoreOnPushNotificationTrait.java new file mode 100644 index 0000000000..2482a4e3ce --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/IgnoreOnPushNotificationTrait.java @@ -0,0 +1,116 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.impl.IgnoreNotifyCommit; +import hudson.scm.SCM; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +public class IgnoreOnPushNotificationTrait extends SCMSourceTrait { + + /** + * Stapler constructor. + */ + @DataBoundConstructor + public IgnoreOnPushNotificationTrait() { + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + ((GitSCMSourceContext) context).withIgnoreOnPushNotifications(true); + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + // this next should be strictly not necessary, but we add it anyway just to be safe + ((GitSCMBuilder) builder).withExtension(new IgnoreNotifyCommit()); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Ignore on push notifications"; + } + + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return GitSCMSource.class; + } + + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/LocalBranchTrait.java b/src/main/java/jenkins/plugins/git/traits/LocalBranchTrait.java new file mode 100644 index 0000000000..d836fb53ba --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/LocalBranchTrait.java @@ -0,0 +1,78 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.LocalBranch; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes the subset of {@link LocalBranch} that is appropriate in the context of a {@link SCMSource} as a + * {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class LocalBranchTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + */ + @DataBoundConstructor + public LocalBranchTrait() { + super(new LocalBranch("**")); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Check out to matching local branch"; + } + + /** + * {@inheritDoc} + */ + @Override + public SCMSourceTrait convertToTrait(@NonNull GitSCMExtension extension) { + LocalBranch ext = (LocalBranch) extension; + if ("**".equals(StringUtils.defaultIfBlank(ext.getLocalBranch(), "**"))) { + return new LocalBranchTrait(); + } + // does not make sense to have any other type of LocalBranch in the context of SCMSource + return null; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/PruneStaleBranchTrait.java b/src/main/java/jenkins/plugins/git/traits/PruneStaleBranchTrait.java new file mode 100644 index 0000000000..c87a0b73c6 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/PruneStaleBranchTrait.java @@ -0,0 +1,73 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.PruneStaleBranch; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link PruneStaleBranch} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class PruneStaleBranchTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + */ + @DataBoundConstructor + public PruneStaleBranchTrait() { + super(new PruneStaleBranch()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + if (context instanceof GitSCMSourceContext) { + GitSCMSourceContext ctx = (GitSCMSourceContext) context; + ctx.pruneRefs(true); + } + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Prune stale remote-tracking branches"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait.java b/src/main/java/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait.java new file mode 100644 index 0000000000..d7e92e8d94 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait.java @@ -0,0 +1,264 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Util; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.plugins.git.GitSCM; +import hudson.scm.SCM; +import hudson.util.FormValidation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.transport.RefSpec; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +/** + * Exposes the ref specs of a {@link AbstractGitSCMSource} as a {@link SCMSourceTrait}. + * The ref specs are stored as templates which are intended to be realised by applying + * {@link String#replaceAll(String, String)} with the {@link AbstractGitSCMSource#REF_SPEC_REMOTE_NAME_PLACEHOLDER} + * pattern to inject the remote name (which should default to {@link AbstractGitSCMSource#DEFAULT_REMOTE_NAME} + * + * @since 3.4.0 + */ +public class RefSpecsSCMSourceTrait extends SCMSourceTrait { + /** + * The ref spec templates. + */ + @NonNull + private final List templates; + + /** + * Stapler constructor. + * + * @param templates the templates. + */ + @DataBoundConstructor + public RefSpecsSCMSourceTrait(@CheckForNull List templates) { + this.templates = new ArrayList<>(Util.fixNull(templates)); + } + + /** + * Utility constructor. + * + * @param templates the template strings. + */ + public RefSpecsSCMSourceTrait(String... templates) { + this.templates = new ArrayList<>(templates.length); + for (String t : templates) { + this.templates.add(new RefSpecTemplate(t)); + } + } + + /** + * Gets the templates. + * + * @return the templates. + */ + @NonNull + public List getTemplates() { + return Collections.unmodifiableList(templates); + } + + /** + * Unwraps the templates. + * + * @return the templates. + */ + public List asStrings() { + List result = new ArrayList<>(templates.size()); + for (RefSpecTemplate t : templates) { + result.add(t.getValue()); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + for (RefSpecTemplate template : templates) { + ((GitSCMSourceContext) context).withRefSpec(template.getValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + for (RefSpecTemplate template : templates) { + ((GitSCMBuilder) builder).withRefSpec(template.getValue()); + } + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Specify ref specs"; + } + + /** + * Returns the default templates. + * + * @return the default templates. + */ + public List getDefaultTemplates() { + return Collections.singletonList(new RefSpecTemplate(AbstractGitSCMSource.REF_SPEC_DEFAULT)); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + } + + /** + * Represents a single wrapped template for easier form binding. + * + * @since 3.4.0 + */ + public static class RefSpecTemplate extends AbstractDescribableImpl { + /** + * The wrapped template value. + */ + @NonNull + private final String value; + + /** + * Stapler constructor. + * + * @param value the template to wrap. + */ + @DataBoundConstructor + public RefSpecTemplate(@NonNull String value) { + this.value = StringUtils.trim(value); + } + + /** + * Gets the template value. + * + * @return the template value. + */ + @NonNull + public String getValue() { + return value; + } + + /** + * The {@link Descriptor} for {@link RefSpecTemplate}. + * + * @since 3.4.0 + */ + @Extension + public static class DescriptorImpl extends Descriptor { + + /** + * Form validation for {@link RefSpecTemplate#getValue()} + * + * @param value the value to check. + * @return the validation result. + */ + @Restricted(NoExternalUse.class) // stapler + public FormValidation doCheckValue(@QueryParameter String value) { + if (StringUtils.isBlank(value)) { + return FormValidation.error("No ref spec provided"); + } + value = StringUtils.trim(value); + try { + String spec = value.replaceAll(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER, "origin"); + if (spec.contains("@{")) { + return FormValidation.errorWithMarkup("Invalid placeholder only " + + Util.escape(AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR) + + " is supported as a placeholder for the remote name"); + } + new RefSpec(spec); + if (!value.startsWith("+")) { + return FormValidation.warningWithMarkup( + "It is recommended to ensure references are always updated by prefixing with " + + "+" + ); + } + return FormValidation.ok(); + } catch (IllegalArgumentException e) { + return FormValidation.error(e.getMessage()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Ref Spec"; + } + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait.java b/src/main/java/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait.java new file mode 100644 index 0000000000..7c7b134725 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait.java @@ -0,0 +1,240 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.plugins.git.GitSCM; +import hudson.scm.SCM; +import hudson.util.FormValidation; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +/** + * Exposes the remote name used for the fetch in a {@link AbstractGitSCMSource} as a {@link SCMSourceTrait}. + * When not provided in the {@link AbstractGitSCMSource#getTraits()} the remote name should default to + * {@link AbstractGitSCMSource#DEFAULT_REMOTE_NAME} + * + * @since 3.4.0 + */ +public class RemoteNameSCMSourceTrait extends SCMSourceTrait { + + /** + * The remote name. + */ + @NonNull + private final String remoteName; + + /** + * Stapler constructor. + * + * @param remoteName the remote name. + */ + @DataBoundConstructor + public RemoteNameSCMSourceTrait(@CheckForNull String remoteName) { + this.remoteName = validate(StringUtils.defaultIfBlank( + StringUtils.trimToEmpty(remoteName), + AbstractGitSCMSource.DEFAULT_REMOTE_NAME + )); + } + + /** + * Gets the remote name. + * + * @return the remote name. + */ + @NonNull + public String getRemoteName() { + return remoteName; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + ((GitSCMSourceContext) context).withRemoteName(remoteName); + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + ((GitSCMBuilder) builder).withRemoteName(remoteName); + } + + /** + * Validate a remote name. + * + * @param value the name. + * @return the name. + * @throws IllegalArgumentException if the name is not valid. + */ + @NonNull + private static String validate(@NonNull String value) { + // see https://github.com/git/git/blob/027a3b943b444a3e3a76f9a89803fc10245b858f/refs.c#L61-L68 + /* + * - any path component of it begins with ".", or + * - it has double dots "..", or + * - it has ASCII control characters, or + * - it has ":", "?", "[", "\", "^", "~", SP, or TAB anywhere, or + * - it has "*" anywhere unless REFNAME_REFSPEC_PATTERN is set, or + * - it ends with a "/", or + * - it ends with ".lock", or + * - it contains a "@{" portion + */ + if (value.contains("..")) { + throw new IllegalArgumentException("Remote name cannot contain '..'"); + } + if (value.contains("//")) { + throw new IllegalArgumentException("Remote name cannot contain empty path segments"); + } + if (value.endsWith("/")) { + throw new IllegalArgumentException("Remote name cannot end with '/'"); + } + if (value.startsWith("/")) { + throw new IllegalArgumentException("Remote name cannot start with '/'"); + } + if (value.endsWith(".lock")) { + throw new IllegalArgumentException("Remote name cannot end with '.lock'"); + } + if (value.contains("@{")) { + throw new IllegalArgumentException("Remote name cannot contain '@{'"); + } + for (String component : StringUtils.split(value, '/')) { + if (component.startsWith(".")) { + throw new IllegalArgumentException("Remote name cannot contain path segments starting with '.'"); + } + if (component.endsWith(".lock")) { + throw new IllegalArgumentException("Remote name cannot contain path segments ending with '.lock'"); + } + } + for (char c : value.toCharArray()) { + if (c < 32) { + throw new IllegalArgumentException("Remote name cannot contain ASCII control characters"); + } + switch (c) { + case ':': + throw new IllegalArgumentException("Remote name cannot contain ':'"); + case '?': + throw new IllegalArgumentException("Remote name cannot contain '?'"); + case '[': + throw new IllegalArgumentException("Remote name cannot contain '['"); + case '\\': + throw new IllegalArgumentException("Remote name cannot contain '\\'"); + case '^': + throw new IllegalArgumentException("Remote name cannot contain '^'"); + case '~': + throw new IllegalArgumentException("Remote name cannot contain '~'"); + case ' ': + throw new IllegalArgumentException("Remote name cannot contain SPACE"); + case '\t': + throw new IllegalArgumentException("Remote name cannot contain TAB"); + case '*': + throw new IllegalArgumentException("Remote name cannot contain '*'"); + } + } + return value; + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Configure remote name"; + } + + /** + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getScmClass() { + return GitSCM.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * Performs form validation for a proposed + * + * @param value the value to check. + * @return the validation results. + */ + @Restricted(NoExternalUse.class) // stapler + public FormValidation doCheckRemoteName(@QueryParameter String value) { + value = StringUtils.trimToEmpty(value); + if (StringUtils.isBlank(value)) { + return FormValidation.error("You must specify a remote name"); + } + if (AbstractGitSCMSource.DEFAULT_REMOTE_NAME.equals(value)) { + return FormValidation.warning("There is no need to configure a remote name of '%s' as " + + "this is the default remote name.", AbstractGitSCMSource.DEFAULT_REMOTE_NAME); + } + try { + validate(value); + return FormValidation.ok(); + } catch (IllegalArgumentException e) { + return FormValidation.error(e.getMessage()); + } + } + + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/SparseCheckoutPathsTrait.java b/src/main/java/jenkins/plugins/git/traits/SparseCheckoutPathsTrait.java new file mode 100644 index 0000000000..0164e7da5c --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/SparseCheckoutPathsTrait.java @@ -0,0 +1,37 @@ +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.SparseCheckoutPaths; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link SparseCheckoutPaths} as a {@link SCMSourceTrait}. + * + * @since 4.0.1 + */ +public class SparseCheckoutPathsTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + * + * @param extension the {@link SparseCheckoutPaths} + */ + @DataBoundConstructor + public SparseCheckoutPathsTrait(SparseCheckoutPaths extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Sparse Checkout paths"; + } + } +} \ No newline at end of file diff --git a/src/main/java/jenkins/plugins/git/traits/SubmoduleOptionTrait.java b/src/main/java/jenkins/plugins/git/traits/SubmoduleOptionTrait.java new file mode 100644 index 0000000000..fe61120eef --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/SubmoduleOptionTrait.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.SubmoduleOption; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link SubmoduleOption} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class SubmoduleOptionTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + * + * @param extension the {@link SubmoduleOption}. + */ + @DataBoundConstructor + public SubmoduleOptionTrait(SubmoduleOption extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Advanced sub-modules behaviours"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java b/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java new file mode 100644 index 0000000000..9c9be145a9 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java @@ -0,0 +1,154 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git.traits; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.plugins.git.GitTagSCMHead; +import jenkins.plugins.git.GitTagSCMRevision; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMHeadAuthority; +import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.TagSCMHeadCategory; +import jenkins.scm.impl.trait.Discovery; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link Discovery} trait for Git that will discover tags on the repository. + * + * @since 3.6.0 + */ +public class TagDiscoveryTrait extends SCMSourceTrait { + /** + * Constructor for stapler. + */ + @DataBoundConstructor + public TagDiscoveryTrait() { + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + GitSCMSourceContext ctx = (GitSCMSourceContext) context; + ctx.wantTags(true); + ctx.withAuthority(new TagSCMHeadAuthority()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean includeCategory(@NonNull SCMHeadCategory category) { + return category instanceof TagSCMHeadCategory; + } + + /** + * Our descriptor. + */ + @Symbol("gitTagDiscovery") + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.TagDiscoveryTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getBuilderClass() { + return GitSCMBuilder.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return GitSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return GitSCMSource.class; + } + } + + /** + * Trusts tags from the repository. + */ + public static class TagSCMHeadAuthority extends SCMHeadAuthority { + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull GitTagSCMHead head) { + return true; + } + + /** + * Out descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.TagDiscoveryTrait_authorityDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Default.class.isAssignableFrom(originClass); + } + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/UserIdentityTrait.java b/src/main/java/jenkins/plugins/git/traits/UserIdentityTrait.java new file mode 100644 index 0000000000..c1e1cf4654 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/UserIdentityTrait.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.UserIdentity; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link UserIdentity} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class UserIdentityTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + * + * @param extension the {@link UserIdentity}. + */ + @DataBoundConstructor + public UserIdentityTrait(UserIdentity extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Custom user name/e-mail address"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/WipeWorkspaceTrait.java b/src/main/java/jenkins/plugins/git/traits/WipeWorkspaceTrait.java new file mode 100644 index 0000000000..2679ee9c90 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/WipeWorkspaceTrait.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.WipeWorkspace; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link WipeWorkspace} as a {@link SCMSourceTrait}. + * + * @since 3.4.0 + */ +public class WipeWorkspaceTrait extends GitSCMExtensionTrait { + /** + * Stapler constructor. + */ + @DataBoundConstructor + public WipeWorkspaceTrait() { + super(new WipeWorkspace()); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Wipe out repository & force clone"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/package-info.java b/src/main/java/jenkins/plugins/git/traits/package-info.java new file mode 100644 index 0000000000..9e0cd8439b --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/package-info.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/** + * The common behaviours that can be used by all {@link jenkins.plugins.git.GitSCMSource} instances and most + * {@link jenkins.plugins.git.AbstractGitSCMSource} instances. + * A lot of these will be effectively simple wrappers over {@link hudson.plugins.git.extensions.GitSCMExtension} + * however we do not want every {@link hudson.plugins.git.extensions.GitSCMExtension} to have a corresponding + * {@link jenkins.plugins.git.traits.GitSCMExtensionTrait} as some of the extensions do not make sense in the context + * of a {@link jenkins.plugins.git.GitSCMSource}. + *

+ * There are some recommendations for {@link hudson.plugins.git.extensions.GitSCMExtension} implementations that are + * being exposed as {@link jenkins.plugins.git.traits.GitSCMExtensionTrait} types: + *

    + *
  • Implement an {@link hudson.plugins.git.extensions.GitSCMExtension#equals(java.lang.Object)}
  • + *
  • Implement a {@link hudson.plugins.git.extensions.GitSCMExtension#hashCode()} returning {@link java.lang.Class#hashCode()}
  • + *
  • Implement {@link hudson.plugins.git.extensions.GitSCMExtension#toString()}
  • + *
+ * + * @since 3.4.0 + */ +package jenkins.plugins.git.traits; diff --git a/src/main/resources/hudson/plugins/git/BranchSpec/config.groovy b/src/main/resources/hudson/plugins/git/BranchSpec/config.groovy index 4b911c2c22..3c4ed176cc 100644 --- a/src/main/resources/hudson/plugins/git/BranchSpec/config.groovy +++ b/src/main/resources/hudson/plugins/git/BranchSpec/config.groovy @@ -1,4 +1,4 @@ -package hudson.plugins.git.BranchSpec; +package hudson.plugins.git.BranchSpec f = namespace(lib.FormTagLib) diff --git a/src/main/resources/hudson/plugins/git/BranchSpec/config_it.properties b/src/main/resources/hudson/plugins/git/BranchSpec/config_it.properties new file mode 100644 index 0000000000..17ddb91ad6 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/BranchSpec/config_it.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2017-, Mark Waite +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Branch\ Specifier\ (blank\ for\ 'any')=Ramo (lasciare in bianco per 'alcune') +Add\ Branch=Aggiungi Ramo +Delete\ Branch=Rimuovi Ramo diff --git a/src/main/resources/hudson/plugins/git/BranchSpec/config_ja.properties b/src/main/resources/hudson/plugins/git/BranchSpec/config_ja.properties new file mode 100644 index 0000000000..14a87d7abd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/BranchSpec/config_ja.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Branch\ Specifier\ (blank\ for\ 'any')=\u30d6\u30e9\u30f3\u30c1\u6307\u5b9a\u5b50 (\u7a7a\u6b04\u306f\u3059\u3079\u3066\u3092\u6307\u5b9a) +Add\ Branch=\u30d6\u30e9\u30f3\u30c1\u306e\u8ffd\u52a0 +Delete\ Branch=\u30d6\u30e9\u30f3\u30c1\u306e\u524a\u9664 diff --git a/src/main/resources/hudson/plugins/git/BranchSpec/help-name.html b/src/main/resources/hudson/plugins/git/BranchSpec/help-name.html index 7c6720ff14..7c39aee1fd 100644 --- a/src/main/resources/hudson/plugins/git/BranchSpec/help-name.html +++ b/src/main/resources/hudson/plugins/git/BranchSpec/help-name.html @@ -1,16 +1,95 @@
- Specify the branches if you'd like to track a specific branch in a repository. - If left blank, all branches will be examined for changes and built.
-
- The syntax is of the form: REPOSITORYNAME/BRANCH. - In addition, BRANCH is recognized as a shorthand of */BRANCH, '*' is recognized as a wildcard, - and '**' is recognized as wildcard that includes the separator '/'. Therefore, origin/branches* would - match origin/branches-foo but not origin/branches/foo, while origin/branches** would - match both origin/branches-foo and origin/branches/foo.
-
- If you are using namespaces to structure branches (e.g. feature1/master, or team1/requestA/rel-1.0) you have to - specify the full branch specifier (including "remotes/"): remotes/REPOSITORYNAME/BRANCH/WITH/NAMESPACE.
- E.g. "remotes/origin/feature1/master" -
- A specific revision can be checked out by specifying the SHA1 hash of that revision in this field. -
\ No newline at end of file +

Specify the branches if you'd like to track a specific branch in a repository. + If left blank, all branches will be examined for changes and built.

+ +

The safest way is to use the refs/heads/<branchName> syntax. This way the expected branch + is unambiguous.

+ +

If your branch name has a / in it make sure to use the full reference above. When not presented + with a full path the plugin will only use the part of the string right of the last slash. + Meaning foo/bar will actually match bar.

+ +

If you use a wildcard branch specifier, with a slash (e.g. release/), + you'll need to specify the origin repository in the branch names to + make sure changes are picked up. So e.g. origin/release/

+ +

Possible options: +

    +
  • <branchName>
    + Tracks/checks out the specified branch. If ambiguous the first result is taken, which is not necessarily + the expected one. Better use refs/heads/<branchName>.
    + E.g. master, feature1, ... +
  • +
  • refs/heads/<branchName>
    + Tracks/checks out the specified branch.
    + E.g. refs/heads/master, refs/heads/feature1/master, ... +
  • +
  • <remoteRepoName>/<branchName>
    + Tracks/checks out the specified branch. If ambiguous the first result is taken, which is not necessarily + the expected one.
    + Better use refs/heads/<branchName>.
    + E.g. origin/master +
  • +
  • remotes/<remoteRepoName>/<branchName>
    + Tracks/checks out the specified branch.
    + E.g. remotes/origin/master +
  • +
  • refs/remotes/<remoteRepoName>/<branchName>
    + Tracks/checks out the specified branch.
    + E.g. refs/remotes/origin/master +
  • +
  • <tagName>
    + This does not work since the tag will not be recognized as tag.
    + Use refs/tags/<tagName> instead.
    + E.g. git-2.3.0 +
  • +
  • refs/tags/<tagName>
    + Tracks/checks out the specified tag.
    + E.g. refs/tags/git-2.3.0 +
  • +
  • <commitId>
    + Checks out the specified commit.
    + E.g. 5062ac843f2b947733e6a3b105977056821bd352, 5062ac84, ... +
  • +
  • ${ENV_VARIABLE}
    + It is also possible to use environment variables. In this case the variables are evaluated and the + result is used as described above.
    + E.g. ${TREEISH}, refs/tags/${TAGNAME}, ... +
  • +
  • <Wildcards>
    + The syntax is of the form: REPOSITORYNAME/BRANCH. + In addition, BRANCH is recognized as a shorthand of */BRANCH, '*' is recognized as a wildcard, + and '**' is recognized as wildcard that includes the separator '/'. Therefore, origin/branches* would + match origin/branches-foo but not origin/branches/foo, while origin/branches** would + match both origin/branches-foo and origin/branches/foo. +
  • +
  • :<regular expression>
    + The syntax is of the form: :regexp. + Regular expression syntax in branches to build will only + build those branches whose names match the regular + expression.
    + Examples:
    +
      +
    • :^(?!(origin/prefix)).* +
        +
      • matches: origin or origin/master or origin/feature
      • +
      • does not match: origin/prefix or origin/prefix_123 or origin/prefix-abc
      • +
      +
    • +
    • :origin/release-\d{8} +
        +
      • matches: origin/release-20150101
      • +
      • does not match: origin/release-2015010 or origin/release-201501011 or origin/release-20150101-something
      • +
      +
    • +
    • :^(?!origin/master$|origin/develop$).* +
        +
      • matches: origin/branch1 or origin/branch-2 or origin/master123 or origin/develop-123
      • +
      • does not match: origin/master or origin/develop
      • +
      +
    • +
    +
  • +
+

+ diff --git a/src/main/resources/hudson/plugins/git/BranchSpec/help-name_ja.html b/src/main/resources/hudson/plugins/git/BranchSpec/help-name_ja.html new file mode 100644 index 0000000000..ffd9463bc2 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/BranchSpec/help-name_ja.html @@ -0,0 +1,87 @@ +
+

リポジトリの特定のブランチを追跡したいなら、ブランチを指定してください。 + 空欄のままにすると、すべてのブランチについて変更があるか確認し、ビルドします。

+ +

refs/heads/<ブランチ名>の形式を使用するのが最も安全です。この方法は、指定したいブランチが明確です。

+ +

もし、ブランチ名が/を含む場合は、必ず上の形式をフルパスで使用してください。フルパスでなければ、プラグインは最後のスラッシュの右側の文字列だけ使用します。 + つまり、foo/barなら、barがマッチします。

+ +

ブランチ指定子に、ワイルドカードとスラッシュ(例: release/)と一緒に使用する場合、ブランチ名にoriginのリポジトリを指定する必要があります。 + そして、ブランチ名にoriginリポジトリを指定して、変更が必ず取得されるようにします。 + 例えば、origin/release/など。

+ +

オプション: +

    +
  • <ブランチ名>
    + 指定したブランチを追跡します。もし、取得した結果があいまいで、必ずしも期待しているものではない場合、 + refs/heads/<ブランチ名>を使ってみてください。
    + 例: master, feature1, ... +
  • +
  • refs/heads/<ブランチ名>
    + 指定したブランチ名を追跡します。
    + 例: refs/heads/master, refs/heads/feature1/master, ... +
  • +
  • <リモートリポジトリ名>/<ブランチ名>
    + 指定したブランチを追跡します。もし、取得した結果があいまいで、必ずしも期待しているものではない場合、 + refs/heads/<ブランチ名>を使ってみてください。
    + 例: origin/master +
  • +
  • remotes/<リモートリポジトリ名>/<ブランチ名>
    + 指定したブランチを追跡します。
    + 例: remotes/origin/master +
  • +
  • refs/remotes/<リモートリポジトリ名>/<ブランチ名>
    + 指定したブランチを追跡します。
    + 例: refs/remotes/origin/master +
  • +
  • <タグ名>
    + タグを認識できないため、動作しません。
    + 代わりに、refs/tags/<タグ名>を使用してください。
    + 例: git-2.3.0 +
  • +
  • refs/tags/<タグ名>
    + 指定したタグを追跡します。
    + 例: refs/tags/git-2.3.0 +
  • +
  • <コミットID>
    + 指定したコミットIDをチェックアウトします。
    + 例: 5062ac843f2b947733e6a3b105977056821bd352, 5062ac84, ... +
  • +
  • ${ENV_VARIABLE}
    + 環境変数も使用可能です。この場合、変数は評価され、結果は上記で説明したような値として使用されます。
    + 例: ${TREEISH}, refs/tags/${TAGNAME}, ... +
  • <ワイルドカード>
    + 文法は、リポジトリ名/ブランチ名の形式です。 + 加えて、ブランチ名は、*/ブランチ名の省略と扱われます。ここで、'*'はワイルドカードとして扱われ、 + '**'はセパレータ'/'を含むワルドカードとして扱われます。それゆえ、origin/branches*は、origin/branches-fooに合致しますが、 + origin/branches/fooには合致しません。 + 一方、origin/branches**は、origin/branches-fooorigin/branches/fooの両方に一致します。 +
  • +
  • :<正規表現>
    + 文法は、:regexpの形式です。 + ここでの正規表現は、ブランチ名がその正規表現に合致するブランチだけをビルドします。
    + 例:
    +
      +
    • :^(?!(origin/prefix)).* +
        +
      • 合致する: originorigin/masterorigin/feature
      • +
      • 合致しない: origin/prefixorigin/prefix_123origin/prefix-abc
      • +
      +
    • +
    • :origin/release-\d{8} +
        +
      • 合致する: origin/release-20150101
      • +
      • 合致しない: origin/release-2015010origin/release-201501011origin/release-20150101-something
      • +
      +
    • +
    • :^(?!origin/master$|origin/develop$).* +
        +
      • 合致する: origin/branch1origin/branch-2origin/master123origin/develop-123
      • +
      • 合致しない: origin/masterorigin/develop
      • +
      +
    • +
    +
  • +
+
diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config.jelly b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config.jelly new file mode 100644 index 0000000000..5d7b2dcbd0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config.jelly @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_it.properties b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_it.properties new file mode 100644 index 0000000000..5abdc72e6d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_it.properties @@ -0,0 +1,2 @@ +Name\ of\ repository=Nome del deposito +Name\ of\ branch=Nome del ramo diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_ja.properties b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_ja.properties new file mode 100644 index 0000000000..797e440318 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/config_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name\ of\ repository=\u30ea\u30dd\u30b8\u30c8\u30ea\u540d +Name\ of\ branch=\u30d6\u30e9\u30f3\u30c1\u540d diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote.html b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote.html new file mode 100644 index 0000000000..771225ebb0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote.html @@ -0,0 +1,3 @@ +
+ Name of the repository, such as origin, that contains the branch you specify below. +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote_ja.html b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote_ja.html new file mode 100644 index 0000000000..8ac0ba3f18 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareRemote_ja.html @@ -0,0 +1,3 @@ +
+ originのような、次の欄で指定したブランチを含むリポジトリの名称です。 +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget.html b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget.html new file mode 100644 index 0000000000..77f25c6cbd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget.html @@ -0,0 +1,3 @@ +
+ The name of the branch within the named repository to compare against. +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget_ja.html b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget_ja.html new file mode 100644 index 0000000000..1f04328822 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/ChangelogToBranchOptions/help-compareTarget_ja.html @@ -0,0 +1,3 @@ +
+ 比較対象の名前付きリポジトリのブランチの名称です。 +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitBranchSpecifierColumn/column.jelly b/src/main/resources/hudson/plugins/git/GitBranchSpecifierColumn/column.jelly new file mode 100644 index 0000000000..d70059414d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitBranchSpecifierColumn/column.jelly @@ -0,0 +1,7 @@ + + + + + ${it.breakOutString(branchSpec)} + + diff --git a/src/main/resources/hudson/plugins/git/GitBranchTokenMacro/help.jelly b/src/main/resources/hudson/plugins/git/GitBranchTokenMacro/help.jelly index 0949fae0ea..b481cce052 100644 --- a/src/main/resources/hudson/plugins/git/GitBranchTokenMacro/help.jelly +++ b/src/main/resources/hudson/plugins/git/GitBranchTokenMacro/help.jelly @@ -1,3 +1,4 @@ +
$${GIT_BRANCH}
diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/digest.jelly b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest.jelly index 1e25d7ae67..72c9c8410b 100644 --- a/src/main/resources/hudson/plugins/git/GitChangeSetList/digest.jelly +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest.jelly @@ -1,22 +1,23 @@ + - + - No changes. + ${%No changes.} - Changes + ${%Changes}
    - +
  1. - ${cs.msgAnnotated} - (detail + + (${%details} / diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_it.properties b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_it.properties new file mode 100644 index 0000000000..3571edcb40 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_it.properties @@ -0,0 +1,3 @@ +No\ changes.=Nessun cambiamenti. +Changes=Cambiamenti +details=dettagli diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_ja.properties b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_ja.properties new file mode 100644 index 0000000000..f82959c433 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/digest_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +No\ changes.=\u5909\u66f4\u306a\u3057 +Changes=\u5909\u66f4\u5c65\u6b74 diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/index.jelly b/src/main/resources/hudson/plugins/git/GitChangeSetList/index.jelly index c6c8460192..b69b0fe655 100644 --- a/src/main/resources/hudson/plugins/git/GitChangeSetList/index.jelly +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/index.jelly @@ -1,23 +1,23 @@ + - + -

    Summary

    +

    ${%Summary}

      - -
    1. ${cs.msgAnnotated} (details)
    2. + +
    3. (${%details})
    - + @@ -39,10 +39,10 @@ diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/index_it.properties b/src/main/resources/hudson/plugins/git/GitChangeSetList/index_it.properties new file mode 100644 index 0000000000..c3a26a4999 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/index_it.properties @@ -0,0 +1,4 @@ +Summary=Riassunto +details=dettagli +Commit=Cambiamento +diff=modifiche diff --git a/src/main/resources/hudson/plugins/git/GitChangeSetList/index_ja.properties b/src/main/resources/hudson/plugins/git/GitChangeSetList/index_ja.properties new file mode 100644 index 0000000000..7595b5e2ab --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitChangeSetList/index_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Summary=\u6982\u8981 +Commit=\u30b3\u30df\u30c3\u30c8 diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/config.jelly b/src/main/resources/hudson/plugins/git/GitPublisher/config.jelly index b82bbde812..57b0a7616c 100644 --- a/src/main/resources/hudson/plugins/git/GitPublisher/config.jelly +++ b/src/main/resources/hudson/plugins/git/GitPublisher/config.jelly @@ -1,19 +1,5 @@ + - @@ -23,6 +9,11 @@ description="${%If pre-build merging is configured, push the result back to the origin}"> + + + @@ -53,7 +44,7 @@
    - -
    - - Commit +
    + + ${%Commit} ${cs.id} @@ -26,11 +26,11 @@ ${cs.id} by ${cs.author} - + in ${cs.branch} -
    ${cs.commentAnnotated}
    +
    ${p.path} - - + + - (diff) + (${%diff})
    - +
    @@ -73,9 +64,13 @@ + + +
    - +
    @@ -104,10 +99,10 @@
    - +
    -
    \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/config_it.properties b/src/main/resources/hudson/plugins/git/GitPublisher/config_it.properties new file mode 100644 index 0000000000..9d2387023e --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/config_it.properties @@ -0,0 +1,26 @@ +Push\ Only\ If\ Build\ Succeeds=Spingere solo quando progetto riesca +Force\ Push=Forzare la spinta +Add\ force\ option\ to\ git\ push=Aggiungi opzione a costringere git push +Tags=Etichette +Tags\ to\ push\ to\ remote\ repositories=Etichette a spingere ai depositi remoti +Add\ Tag=Aggiungi Ettichetta +Tag\ to\ push=Etichetta a spingere +Tag\ message=Messagio dell\'etichetta +Create\ new\ tag=Crea nuova etichetta +Update\ new\ tag=Modifica nuova etichetta +Target\ remote\ name=Nome di deposito remoto +Delete\ Tag=Remuovi Etichetta +Branches=Rami +Branches\ to\ push\ to\ remote\ repositories=Rami a spingere ai depositi remoti +Add\ Branch=Aggiungi Ramo +Branch\ to\ push=Ramo a spingere +Delete\ Branch=Rimuovi Ramo +Notes=Note +Notes\ to\ push\ to\ remote\ repositories=Noti a spingere ai depositi remoti +Add\ Note=Aggiungi Nota +Note\ to\ push=Nota a spingere +Note's\ namespace=Spazio di nome della nota +Abort\ if\ note\ exists=Interrompe se nota esiste +Merge\ Results=Resultati di funire +If\ pre-build\ merging\ is\ configured,\ push\ the\ result\ back\ to\ the\ origin= +Delete\ Note=Rimuovi Nota diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/config_ja.properties b/src/main/resources/hudson/plugins/git/GitPublisher/config_ja.properties new file mode 100644 index 0000000000..8d07d67de4 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/config_ja.properties @@ -0,0 +1,49 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Push\ Only\ If\ Build\ Succeeds=\u30d3\u30eb\u30c9\u6210\u529f\u6642\u306e\u307f\u30d7\u30c3\u30b7\u30e5 +Merge\ Results=\u7d50\u679c\u3092\u30de\u30fc\u30b8 +If\ pre-build\ merging\ is\ configured,\ push\ the\ result\ back\ to\ the\ origin=\ +\u3000\u30d7\u30ec\u30d3\u30eb\u30c9\u306e\u30de\u30fc\u30b8\u3092\u8a2d\u5b9a\u3057\u3066\u3044\u308b\u5834\u5408\u3001\u7d50\u679c\u3092origin\u306b\u30d7\u30c3\u30b7\u30e5\u3059\u308b +Force\ Push=\u5f37\u5236\u30d7\u30c3\u30b7\u30e5 +Add\ force\ option\ to\ git\ push=git push\u30b3\u30de\u30f3\u30c9\u306b\u5f37\u5236\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8ffd\u52a0\u3059\u308b +Tags=\u30bf\u30b0 +Add\ Tag=\u30bf\u30b0\u306e\u8ffd\u52a0 +Tags\ to\ push\ to\ remote\ repositories=\u30ea\u30e2\u30fc\u30c8\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30bf\u30b0 +Tag\ to\ push=\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30bf\u30b0 +Tag\ message=\u30bf\u30b0\u306e\u30e1\u30c3\u30bb\u30fc\u30b8 +Create\ new\ tag=\u30bf\u30b0\u306e\u4f5c\u6210 +Update\ new\ tag=\u30bf\u30b0\u306e\u66f4\u65b0 + +Branches=\u30d6\u30e9\u30f3\u30c1 +Add\ Branch=\u30d6\u30e9\u30f3\u30c1\u306e\u8ffd\u52a0 +Branches\ to\ push\ to\ remote\ repositories=\u30ea\u30e2\u30fc\u30c8\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30d6\u30e9\u30f3\u30c1 +Branch\ to\ push=\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30d6\u30e9\u30f3\u30c1 + +Notes=\u30ce\u30fc\u30c8 +Add\ Note=\u30ce\u30fc\u30c8\u306e\u8ffd\u52a0 +Notes\ to\ push\ to\ remote\ repositories=\u30ea\u30e2\u30fc\u30c8\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30ce\u30fc\u30c8 +Note\ to\ push=\u30d7\u30c3\u30b7\u30e5\u3059\u308b\u30ce\u30fc\u30c8 +Target\ remote\ name=\u5bfe\u8c61\u306e\u30ea\u30e2\u30fc\u30c8\u540d +Note's\ namespace=\u30ce\u30fc\u30c8\u306e\u540d\u524d\u7a7a\u9593 +Abort\ if\ note\ exists=\u30ce\u30fc\u30c8\u304c\u3059\u3067\u306b\u5b58\u5728\u3059\u308b\u5834\u5408\u4e2d\u6b62 + diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/help-branchesToPush_ja.html b/src/main/resources/hudson/plugins/git/GitPublisher/help-branchesToPush_ja.html new file mode 100644 index 0000000000..b2d8ec2c53 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/help-branchesToPush_ja.html @@ -0,0 +1,6 @@ +
    + ビルドの完了時に、カレントのHEADをプッシュするリモートのブランチを指定します。
    + ブランチ名はリモートブランチの名前です。
    + ブランチ名には、環境変数を使用可能です。- それらは、ビルド時に置き換えられます。
    + リポジトリ名は、ソースコード管理で設定したリポジトリのうちの1つでなければなりません。 +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/help-notesToPush_ja.html b/src/main/resources/hudson/plugins/git/GitPublisher/help-notesToPush_ja.html new file mode 100644 index 0000000000..1d71fd888e --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/help-notesToPush_ja.html @@ -0,0 +1,10 @@ +
    + ビルドの完了時にプッシュするノートを指定します。 + ノートには環境変数を使用可能で、ビルド時に差し替えられます。 + ノートの名前空間とリモートリポジトリの名称はオプションです。それらは、デフォルトで、masterとoriginです。 + +

    + ノートのメッセージに使用可能な環境変数:
    + $BUILDRESULT : ビルド結果。ビルド後の処理の結果は含まない。
    + $BUILDDURATION : ビルドにかかった時間。ビルド後の処理にかかった時間は含まない。
    +
    diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/help-pushMerge_ja.html b/src/main/resources/hudson/plugins/git/GitPublisher/help-pushMerge_ja.html new file mode 100644 index 0000000000..db98af1e9a --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/help-pushMerge_ja.html @@ -0,0 +1,3 @@ +
    + プレビルドマージのオプションで指定したoriginにマージした結果をプッシュします。 +
    diff --git a/src/main/resources/hudson/plugins/git/GitPublisher/help-pushOnlyIfSuccess_ja.html b/src/main/resources/hudson/plugins/git/GitPublisher/help-pushOnlyIfSuccess_ja.html new file mode 100644 index 0000000000..ee48a08246 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitPublisher/help-pushOnlyIfSuccess_ja.html @@ -0,0 +1,3 @@ +
    + ビルドが成功した場合、リモートにプッシュだけします。成功でないなら、何もプッシュされません。 +
    diff --git a/src/main/resources/hudson/plugins/git/GitRevisionBuildParameters/config.jelly b/src/main/resources/hudson/plugins/git/GitRevisionBuildParameters/config.jelly index 0a0e219f80..86a377f00f 100644 --- a/src/main/resources/hudson/plugins/git/GitRevisionBuildParameters/config.jelly +++ b/src/main/resources/hudson/plugins/git/GitRevisionBuildParameters/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/hudson/plugins/git/GitSCM/global_it.properties b/src/main/resources/hudson/plugins/git/GitSCM/global_it.properties new file mode 100644 index 0000000000..7df2ee8e0f --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/global_it.properties @@ -0,0 +1,4 @@ +Git\ plugin=Plugin Git +Global\ Config\ user.name\ Value=Valore della configurazione globale user.name +Global\ Config\ user.email\ Value=Valore della configurazione globale user.email +Create\ new\ accounts\ based\ on\ author/committer's\ email=Crea conti nuovi con l'indirrizzo dell'autore diff --git a/src/main/resources/hudson/plugins/git/GitSCM/global_ja.properties b/src/main/resources/hudson/plugins/git/GitSCM/global_ja.properties new file mode 100644 index 0000000000..c653f4d82c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/global_ja.properties @@ -0,0 +1,29 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Git\ plugin=Git Plugin +Global\ Config\ user.name\ Value=\u30b0\u30ed\u30fc\u30d0\u30eb\u306auser.name\u306e\u5024 +Global\ Config\ user.email\ Value=\u30b0\u30ed\u30fc\u30d0\u30eb\u306auser.email\u306e\u5024 +Create\ new\ accounts\ base\ on\ author/committer's\ email=email\u3092\u3082\u3068\u306b\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f5c\u6210 +Default\ git\ client\ implementation=\u30c7\u30d5\u30a9\u30eb\u30c8\u306eGit\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306e\u5b9f\u88c5 +JGit=JGit +command\ line\ Git=\u30b3\u30de\u30f3\u30c9\u30e9\u30a4\u30f3\u306eGit diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-choosingStrategy.html b/src/main/resources/hudson/plugins/git/GitSCM/help-choosingStrategy.html index f4934197e6..42224eda5b 100644 --- a/src/main/resources/hudson/plugins/git/GitSCM/help-choosingStrategy.html +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-choosingStrategy.html @@ -11,7 +11,7 @@
    This does the opposite of the "Default" strategy — any branches listed under "Branches to build" will not be built. For example, entering the pattern - */master will cause Jenkins to build any changes found on any branch, so long as the + */master will cause Jenkins to build any changes found on any branch, so long as the branch name does not match this pattern.
    If multiple branches match these criteria, the oldest will be selected.
    diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail.html b/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail.html index 3877a0496e..43b66c8fe3 100644 --- a/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail.html +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail.html @@ -1,4 +1,4 @@

    - As git changelog is parsed to identify authors/committers and populate jenkins user database, use email as ID for - new users. -

    \ No newline at end of file + If checked, upon parsing of git change logs, new user accounts are created on demand for the identified + committers / authors in the internal Jenkins database. The e-mail address is used as the id of the account. +

    diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail_ja.html b/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail_ja.html new file mode 100644 index 0000000000..b5107087e9 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-createAccountBasedOnEmail_ja.html @@ -0,0 +1,4 @@ +

    + Gitの変更履歴を解析し、著者名やコミット名を認識し、Jenkinsのユーザデータベースに登録し、 + メールアドレスを新規ユーザのIDとします。 +

    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigEmail_ja.html b/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigEmail_ja.html new file mode 100644 index 0000000000..48daba6334 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigEmail_ja.html @@ -0,0 +1,4 @@ +
    +

    ビルドの前に"git config user.email [this]"が呼ばれます。 + これは、各プロジェクトでの値で上書きされます。

    +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigName_ja.html b/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigName_ja.html new file mode 100644 index 0000000000..52f798ee96 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-globalConfigName_ja.html @@ -0,0 +1,4 @@ +
    +

    ビルドの前に"git config user.name [this]"が呼ばれます。 + これは、各プロジェクトでの値で上書きされます。

    +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-useExistingAccountWithSameEmail.html b/src/main/resources/hudson/plugins/git/GitSCM/help-useExistingAccountWithSameEmail.html new file mode 100644 index 0000000000..ea4466d39c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-useExistingAccountWithSameEmail.html @@ -0,0 +1,6 @@ +

    + Will make sure that user will be searched by their actual e-mail address before resorting to creating the user with its mail address as id.
    + This allows instances using specific user realms to match their users based on the assumption that same mail address means same user.
    +
    + Note that this behavior requires mailer plugin to be installed. +

    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs.html b/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs.html index b2bdcc0140..1a5dd14a29 100644 --- a/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs.html +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs.html @@ -16,24 +16,24 @@ is bare or non-bare (i.e. has a working directory).
    • If the super-project is bare, the location of the submodules will be - taken from .gitmodules.
    • -
    • If the super-project is not bare, it is assumed that the + taken from .gitmodules.
    • +
    • If the super-project is not bare, it is assumed that the repository has each of its submodules cloned and checked out appropriately. Thus, the submodules will be taken directly from a path like ${SUPER_PROJECT_URL}/${SUBMODULE}, rather than relying on - information from .gitmodules.
    • + information from .gitmodules.
    For a local URL/path to a super-project, - git rev-parse --is-bare-repository + git rev-parse --is-bare-repository is used to detect whether the super-project is bare or not.
    For a remote URL to a super-project, the ending of the URL determines whether a bare or non-bare repository is assumed:
      -
    • If the remote URL ends with /.git, a non-bare repository is +
    • If the remote URL ends with /.git, a non-bare repository is assumed.
    • -
    • If the remote URL does NOT end with /.git, a bare +
    • If the remote URL does NOT end with /.git, a bare repository is assumed.
    diff --git a/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs_ja.html b/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs_ja.html new file mode 100644 index 0000000000..b0c71552f7 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/GitSCM/help-userRemoteConfigs_ja.html @@ -0,0 +1,32 @@ +
    + 追跡するリポジトリを指定します。URLかローカルなファイルパスを使用可能です。 + スーパープロジェクト(サブモジュールを持つリポジトリ)の場合、 + ローカルなファイルパスか絶対パスが有効です。 + 有効なGitのURLの例を以下に示します。 +
      +
    • ssh://git@github.com/github/git.git
    • +
    • git@github.com:github/git.git (SSHプロトコルの短縮記法)
    • +
    • ssh://user@other.host.com/~/repos/R.git (ホームディレクトリのrepos/R.gitリポジトリへのアクセス)
    • +
    • https://github.com/github/git.git
    • +
    • git://github.com/github/git.git
    • +
    +
    + リポジトリがスーパープロジェクトの場合、サブモジュールをクローンする場所は、 + リポジトリがベアかノンベアか(すなわち、ワーキングディレクトリがあるかどうか)によって異なります。 +
      +
    • スーパープロジェクトがベアの場合、サブモジュールの位置は、.gitmodulesから取得します。
    • +
    • スーパープロジェクトがベアでない場合、 リポジトリには、サブモジュールがクローンされ、 + 適切にチェックアウトされているものとします。 + したがって、サブモジュールは、.gitmodulesの情報ではなく、 + ${SUPER_PROJECT_URL}/${SUBMODULE}のようなパスから直接取得します。
    • +
    + + スーパープロジェクトへのローカルなURLやパスは、スーパープロジェクトがベアか、そうでないかを判別する + git rev-parse --is-bare-repositoryを使用する際に用います。 +
    + スーパプロジェクトへのリモートなURLは、そのURLの最後でベアかベアでないかを判別します。 +
      +
    • リモートURLが.gitで終わる場合、ノンベアリポジトリではないと想定されます。
    • +
    • リモートURLが.gitで終わらない場合、ベアリポジトリと想定されます。
    • +
    +
    diff --git a/src/main/resources/hudson/plugins/git/GitSCM/project-changes.jelly b/src/main/resources/hudson/plugins/git/GitSCM/project-changes.jelly index 61842565d2..72d9fc3294 100644 --- a/src/main/resources/hudson/plugins/git/GitSCM/project-changes.jelly +++ b/src/main/resources/hudson/plugins/git/GitSCM/project-changes.jelly @@ -1,3 +1,4 @@ + - + + - + - - + + + ${it.toString()} + - \ No newline at end of file + + + ${it.toString()} + + + diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/config_it.properties b/src/main/resources/hudson/plugins/git/UserMergeOptions/config_it.properties new file mode 100644 index 0000000000..1bdb3e6aca --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/config_it.properties @@ -0,0 +1,4 @@ +Name\ of\ repository=Nome di deposito +Branch\ to\ merge\ to=Ramo a fusare +Merge\ strategy=Strategia di fusione +Fast-forward\ mode=Moda fast-forward diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/config_ja.properties b/src/main/resources/hudson/plugins/git/UserMergeOptions/config_ja.properties new file mode 100644 index 0000000000..a183e7bebc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/config_ja.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name\ of\ repository=\u30ea\u30dd\u30b8\u30c8\u30ea\u540d +Branch\ to\ merge\ to=\u30de\u30fc\u30b8\u5148\u306e\u30d6\u30e9\u30f3\u30c1 +Merge\ strategy=\u30de\u30fc\u30b8\u65b9\u6cd5 +Fast-forward\ mode diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode.html new file mode 100644 index 0000000000..d82f4f00d9 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode.html @@ -0,0 +1,5 @@ +
    + Merge fast-forward mode selection.
    + The default, --ff, gracefully falls back to a merge commit when required.
    + For more information, see the Git Merge Documentation +
    diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode_ja.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode_ja.html new file mode 100644 index 0000000000..96d571c043 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-fastForwardMode_ja.html @@ -0,0 +1,5 @@ +
    + Fast-forwardマージモードを選択します。
    + デフォルトである --ff は必要であれば、マージコミットを作成します。
    + より詳細な情報は、 Git Merge Documentationを参照してください。 +
    diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote.html index 28d19739e7..1df13af8af 100644 --- a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote.html +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote.html @@ -1,4 +1,4 @@
    - Name of the repository, such as origin, that contains the branch you specify below. If left blank, + Name of the repository, such as origin, that contains the branch you specify below. If left blank, it'll default to the name of the first repository configured above.
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote_ja.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote_ja.html new file mode 100644 index 0000000000..7e9d5145d7 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeRemote_ja.html @@ -0,0 +1,4 @@ +
    + originのような、下記で指定するブランチを含むリポジトリ名を指定します。 + 未設定の場合、上記で設定した最初のリポジトリの名前がデフォルトとして設定されます。 +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy.html index c18917977a..88f35aaeec 100644 --- a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy.html +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy.html @@ -1,4 +1,4 @@
    Merge strategy selection. - This feature is not fully implemented in JGIT. + This feature is not fully implemented in JGIT.
    diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy_ja.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy_ja.html new file mode 100644 index 0000000000..8e5bc3e2e9 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeStrategy_ja.html @@ -0,0 +1,4 @@ +
    + マージ方法を選択します。 + この機能は、JGITでは十分に実装されていません。 +
    diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget.html index b439466db5..f8ec4daad4 100644 --- a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget.html +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget.html @@ -1,3 +1,3 @@
    - The name of the branch within the named repository to merge to, such as master. + The name of the branch within the named repository to merge to, such as master.
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget_ja.html b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget_ja.html new file mode 100644 index 0000000000..d64ae15e54 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserMergeOptions/help-mergeTarget_ja.html @@ -0,0 +1,3 @@ +
    + masterのような、マージ先のリポジトリ内のブランチ名 +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/config.groovy b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config.groovy index c420edc9f8..bab030c7f7 100644 --- a/src/main/resources/hudson/plugins/git/UserRemoteConfig/config.groovy +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config.groovy @@ -1,10 +1,10 @@ -package hudson.plugins.git.UserRemoteConfig; +package hudson.plugins.git.UserRemoteConfig f = namespace(lib.FormTagLib) c = namespace(lib.CredentialsTagLib) f.entry(title:_("Repository URL"), field:"url") { - f.textbox() + f.textbox(checkMethod: "post") } f.entry(title:_("Credentials"), field:"credentialsId") { @@ -29,6 +29,6 @@ f.advanced { f.entry { div(align:"right") { input (type:"button", value:_("Add Repository"), class:"repeatable-add show-if-last") - input (type:"button", value:_("Delete Repository"), class:"repeatable-delete") + input (type:"button", value:_("Delete Repository"), class:"repeatable-delete show-if-not-only") } } diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_it.properties b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_it.properties new file mode 100644 index 0000000000..e10266d3e4 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_it.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2017-, Mark Waite +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Repository\ URL=URL di Deposito +Credentials=Credenziali +Name=Nome +Refspec=Refspec +Add\ Repository=Aggiungi Deposito +Delete\ Repository=Rimuovi Deposito diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_ja.properties b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_ja.properties new file mode 100644 index 0000000000..71c0623ac0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/config_ja.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Repository\ URL=\u30ea\u30dd\u30b8\u30c8\u30eaURL +Credentials=\u8a8d\u8a3c\u60c5\u5831 +Name=\u540d\u79f0 +Refspec=Refspec +Add\ Repository=\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u8ffd\u52a0 +Delete\ Repository=\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u524a\u9664 diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name.html index 26a653c021..38e3ab8ec3 100644 --- a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name.html +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name.html @@ -1,6 +1,6 @@
    - ID of the repository, such as origin, to uniquely identify this repository among other remote repositories. - This is the same "name" that you use in your git remote command. If left empty, + ID of the repository, such as origin, to uniquely identify this repository among other remote repositories. + This is the same "name" that you use in your git remote command. If left empty, Jenkins will generate unique names for you.

    diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name_ja.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name_ja.html new file mode 100644 index 0000000000..9d174e65d2 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-name_ja.html @@ -0,0 +1,7 @@ +

    + 他のリモートリポジトリからこのリポジトリを識別する、originのようなリポジトリのIDです。 + これは、git remoteコマンドで使用する"名称"と同じです。未設定の場合、Jenkinsはユニークな名称を生成します。 + +

    + 複数のリモートリポジトリがあれば、普通この名称を指定したくなるでしょう。 +

    diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec.html index da5449e321..d89f4f6886 100644 --- a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec.html +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec.html @@ -1,16 +1,26 @@
    A refspec controls the remote refs to be retrieved and how they map to local refs. If left blank, it will default to - the normal behaviour of git fetch, which retrieves all the branch heads as remotes/REPOSITORYNAME/BRANCHNAME. + the normal behaviour of git fetch, which retrieves all the branch heads as remotes/REPOSITORYNAME/BRANCHNAME. This default behaviour is OK for most cases.

    - In other words, the default refspec is "+refs/heads/*:refs/remotes/REPOSITORYNAME/*" where REPOSITORYNAME is the value + In other words, the default refspec is "+refs/heads/*:refs/remotes/REPOSITORYNAME/*" where REPOSITORYNAME is the value you specify in the above "name of repository" textbox.

    When do you want to modify this value? A good example is when you want to just retrieve one branch. For example, - +refs/heads/master:refs/remotes/origin/master would only retrieve the master branch and nothing else. + +refs/heads/master:refs/remotes/origin/master would only retrieve the master branch and nothing else.

    - See the term definition in Git user manual for more details. -

    \ No newline at end of file + The plugin uses a default refspec for its initial fetch, unless the "Advanced Clone Option" is set to honor refspec. + This keeps compatibility with previous behavior, and allows the job definition to decide if the refspec should be + honored on initial clone. + +

    + Multiple refspecs can be entered by separating them with a space character. + +refs/heads/master:refs/remotes/origin/master +refs/heads/develop:refs/remotes/origin/develop + retrieves the master branch and the develop branch and nothing else. + +

    + See the refspec definition in Git user manual for more details. +

    diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec_ja.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec_ja.html new file mode 100644 index 0000000000..070e069cf2 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-refspec_ja.html @@ -0,0 +1,16 @@ +
    + refspecは、リモート参照の取得方法と、ローカル参照へマップする方法を制御します。 + 未設定の場合、git fetchの、すべてのブランチのheadをremotes/REPOSITORYNAME/BRANCHNAMEとして取得する動作が、 + デフォルトの動作になります。このデフォルト動作は多くの場合問題ありません。 + +

    + 言い換えると、デフォルトのrefspecは、"+refs/heads/*:refs/remotes/REPOSITORYNAME/*"です。 + ここで、REPOSITORYNAME は、上のテキストボックス"名称"に指定した値です。 + +

    + いつこの値を変更しようと思うでしょうか? 1つのブランチだけを取得したい場合がよい例です。 + 例えば、+refs/heads/master:refs/remotes/origin/masterは、マスタブランチだけを取得します。 + +

    + 詳細は、Gitユーザマニュアルの言葉の定義を参照してください。 +

    diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url.html index d2dfb6aa6b..2eeca3e262 100644 --- a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url.html +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url.html @@ -1,3 +1,3 @@
    - Specify the URL of this remote repository. This uses the same syntax as your git clone command. + Specify the URL of this remote repository. This uses the same syntax as your git clone command.
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url_ja.html b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url_ja.html new file mode 100644 index 0000000000..0abdb808a0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/UserRemoteConfig/help-url_ja.html @@ -0,0 +1,3 @@ +
    + このリモートリポジトリのURLを指定します。git cloneコマンドと同じ文法を使用します。 +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config.jelly new file mode 100644 index 0000000000..012b6f02c6 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config_it.properties new file mode 100644 index 0000000000..0bc0eb3c06 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/config_it.properties @@ -0,0 +1 @@ +Assembla\ Git\ URL=URL Git d'Assembla diff --git a/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/help-repoUrl.html new file mode 100644 index 0000000000..fecc52c276 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/AssemblaWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://www.assembla.com/code/PROJECT/git/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-repoUrl.html new file mode 100644 index 0000000000..317929218b --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://bitbucket.org/OWNER/REPO/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-url.html deleted file mode 100644 index 864cf2481f..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/BitbucketWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the HTTP URL for this repository's Bitbucket page (such as http://bitbucket.org/owner/reponame). -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/CGit/config.jelly b/src/main/resources/hudson/plugins/git/browser/CGit/config.jelly index da4a19e26b..300f16f5c6 100644 --- a/src/main/resources/hudson/plugins/git/browser/CGit/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/CGit/config.jelly @@ -1,6 +1,6 @@ - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/CGit/config_it.properties b/src/main/resources/hudson/plugins/git/browser/CGit/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/CGit/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config.jelly b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config.jelly index c1b799ca02..2e729fb714 100644 --- a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config.jelly @@ -1,5 +1,6 @@ + - - + + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config_it.properties b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-repoUrl.html new file mode 100644 index 0000000000..2a4881ec28 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the URL of this repository in FishEye (such as http://fisheye6.cenqua.com/browse/ant/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-url.html b/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-url.html deleted file mode 100644 index eeb953ba50..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/FisheyeGitRepositoryBrowser/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the URL of this module in FishEye. (such as http://fisheye6.cenqua.com/browse/ant/) -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config.jelly b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config.jelly index f49e198f48..5f8b41bceb 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config.jelly @@ -1,8 +1,9 @@ + - - + + - + diff --git a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config_it.properties new file mode 100644 index 0000000000..ac98c491af --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/config_it.properties @@ -0,0 +1,2 @@ +GitBlit\ root\ url=GitBlit URL di radice +Project\ name\ in\ GitBlit=Nome di progetto a GitBlit diff --git a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-projectName.html b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-projectName.html index 9a168ce001..7e84b5d4ac 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-projectName.html +++ b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-projectName.html @@ -1,4 +1,4 @@
    - Specify the name of the project in GitBlit + Specify the name of the project in GitBlit.
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-url.html b/src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-repoUrl.html similarity index 100% rename from src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-url.html rename to src/main/resources/hudson/plugins/git/browser/GitBlitRepositoryBrowser/help-repoUrl.html diff --git a/src/main/resources/hudson/plugins/git/browser/GitLab/config.jelly b/src/main/resources/hudson/plugins/git/browser/GitLab/config.jelly index d0cd01fd3c..697cf54059 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitLab/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/GitLab/config.jelly @@ -1,8 +1,9 @@ + - + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/GitLab/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GitLab/config_it.properties new file mode 100644 index 0000000000..085a7d9393 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitLab/config_it.properties @@ -0,0 +1,2 @@ +URL=URL +Version=Versione diff --git a/src/main/resources/hudson/plugins/git/browser/GitLab/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/GitLab/help-repoUrl.html new file mode 100644 index 0000000000..d2b0d60371 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitLab/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as http://gitlabserver:port/group/REPO/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitLab/help-url.html b/src/main/resources/hudson/plugins/git/browser/GitLab/help-url.html deleted file mode 100644 index fe86377386..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/GitLab/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (such as http://gitlabserver:port/repo/). -
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitLab/help-version.html b/src/main/resources/hudson/plugins/git/browser/GitLab/help-version.html index a9c5b47ed6..cd01e7c59b 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitLab/help-version.html +++ b/src/main/resources/hudson/plugins/git/browser/GitLab/help-version.html @@ -1,3 +1,4 @@
    - Specify the major and minor version of gitlab you use (such as 3.1). + Specify the major and minor version of GitLab you use (such as 9.1). If you + don't specify a version, a modern version of GitLab (>= 8.0) is assumed.
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/GitList/config.jelly b/src/main/resources/hudson/plugins/git/browser/GitList/config.jelly new file mode 100644 index 0000000000..8f36c3a97d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitList/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/hudson/plugins/git/browser/GitList/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GitList/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitList/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/GitList/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/GitList/help-repoUrl.html new file mode 100644 index 0000000000..d195e2799d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitList/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as http://gitlistserver:port/REPO/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/GitWeb/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/GitWeb/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/GitWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GitWeb/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitWeb/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/GitWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/GitWeb/help-repoUrl.html new file mode 100644 index 0000000000..2fb7cf2b27 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://github.com/jenkinsci/jenkins.git). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/GitWeb/help-url.html deleted file mode 100644 index a364ea3af6..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/GitWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (such as this). -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/GithubWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/GithubWeb/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/GithubWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/GithubWeb/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/GithubWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GithubWeb/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GithubWeb/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-repoUrl.html new file mode 100644 index 0000000000..a389d03b39 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the HTTP URL for this repository's GitHub page (such as https://github.com/jquery/jquery). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-url.html deleted file mode 100644 index 354cd56648..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/GithubWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the HTTP URL for this repository's GitHub page (such as this). -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/Gitiles/config.jelly b/src/main/resources/hudson/plugins/git/browser/Gitiles/config.jelly new file mode 100644 index 0000000000..2e729fb714 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Gitiles/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/hudson/plugins/git/browser/Gitiles/config_it.properties b/src/main/resources/hudson/plugins/git/browser/Gitiles/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Gitiles/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/Gitiles/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/Gitiles/help-repoUrl.html new file mode 100644 index 0000000000..ed4f8c49e1 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Gitiles/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://gwt.googlesource.com/gwt/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-repoUrl.html new file mode 100644 index 0000000000..be7c888e7b --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://gitorious.org/gitorious/mainline). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-url.html deleted file mode 100644 index 7cf22b5cb0..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/GitoriousWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (such as http://gitorious.org/gitorious/mainline). -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/GogsGit/config.jelly b/src/main/resources/hudson/plugins/git/browser/GogsGit/config.jelly new file mode 100644 index 0000000000..8f36c3a97d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GogsGit/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/hudson/plugins/git/browser/GogsGit/config_it.properties b/src/main/resources/hudson/plugins/git/browser/GogsGit/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/GogsGit/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/KilnGit/config.jelly b/src/main/resources/hudson/plugins/git/browser/KilnGit/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/KilnGit/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/KilnGit/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/KilnGit/config_it.properties b/src/main/resources/hudson/plugins/git/browser/KilnGit/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/KilnGit/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/KilnGit/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/KilnGit/help-repoUrl.html new file mode 100644 index 0000000000..e9c8366464 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/KilnGit/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as https://khanacademy.kilnhg.com/Code/Website/Group/webapp). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/KilnGit/help-url.html b/src/main/resources/hudson/plugins/git/browser/KilnGit/help-url.html deleted file mode 100644 index 8c88e8b7c7..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/KilnGit/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (e.g., https://khanacademy.kilnhg.com/Code/Website/Group/webapp) -
    diff --git a/src/main/resources/hudson/plugins/git/browser/Phabricator/config.jelly b/src/main/resources/hudson/plugins/git/browser/Phabricator/config.jelly index 2f2e5f7a1e..2d1e98e51e 100644 --- a/src/main/resources/hudson/plugins/git/browser/Phabricator/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/Phabricator/config.jelly @@ -1,8 +1,9 @@ + - + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/Phabricator/config_it.properties b/src/main/resources/hudson/plugins/git/browser/Phabricator/config_it.properties new file mode 100644 index 0000000000..f367031a3c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Phabricator/config_it.properties @@ -0,0 +1,2 @@ +URL=URL +Repository\ name\ in\ Phab=Nome del deposito a Phab diff --git a/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repo.html b/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repo.html index 9c2b8e5c42..2f6445acc7 100644 --- a/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repo.html +++ b/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repo.html @@ -1,3 +1,3 @@
    - Specify the repository name in phabricator (eg the "foo" part of phabricator.exmaple.com/diffusion/foo/browse) -
    \ No newline at end of file + Specify the repository name in phabricator (such as the foo part of phabricator.example.com/diffusion/foo/browse). + diff --git a/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repoUrl.html new file mode 100644 index 0000000000..3394262701 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Phabricator/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the phabricator instance root URL (such as http://phabricator.example.com). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/Phabricator/help-url.html b/src/main/resources/hudson/plugins/git/browser/Phabricator/help-url.html deleted file mode 100644 index 7436277ea4..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/Phabricator/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the phabricator instance root URL (such as http://phabricator.example.com/). -
    diff --git a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config.jelly index c1b799ca02..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config.jelly @@ -1,5 +1,6 @@ + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-repoUrl.html new file mode 100644 index 0000000000..cf2ba39b9c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as http://SERVER/PATH/projects/PROJECT/repository). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-url.html deleted file mode 100644 index 549587f67e..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/RedmineWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (such as this). -
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/browser/RhodeCode/config.jelly b/src/main/resources/hudson/plugins/git/browser/RhodeCode/config.jelly index 47b4b3008d..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/RhodeCode/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/RhodeCode/config.jelly @@ -1,5 +1,6 @@ + - + diff --git a/src/main/resources/hudson/plugins/git/browser/RhodeCode/config_it.properties b/src/main/resources/hudson/plugins/git/browser/RhodeCode/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/RhodeCode/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-repoUrl.html new file mode 100644 index 0000000000..735423f12c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the HTTP URL for this repository's RhodeCode page (such as http://rhodecode.mydomain.com:5000/projects/PROJECT/repos/REPO/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-url.html b/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-url.html deleted file mode 100644 index db3a5a76f9..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/RhodeCode/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the HTTP URL for this repository's RhodeCode page (such as http://stash.mydomain.com:7990/projects/PROJECT_KEY/repos/myrepo). -
    diff --git a/src/main/resources/hudson/plugins/git/browser/Stash/config.jelly b/src/main/resources/hudson/plugins/git/browser/Stash/config.jelly index 47b4b3008d..8f36c3a97d 100644 --- a/src/main/resources/hudson/plugins/git/browser/Stash/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/Stash/config.jelly @@ -1,5 +1,6 @@ + - + diff --git a/src/main/resources/hudson/plugins/git/browser/Stash/config_it.properties b/src/main/resources/hudson/plugins/git/browser/Stash/config_it.properties new file mode 100644 index 0000000000..6d5d9bcdfc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Stash/config_it.properties @@ -0,0 +1 @@ +URL=URL diff --git a/src/main/resources/hudson/plugins/git/browser/Stash/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/Stash/help-repoUrl.html new file mode 100644 index 0000000000..d493f7b2dd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/Stash/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the HTTP URL for this repository's Stash page (such as http://stash.mydomain.com:7990/projects/PROJECT/repos/REPO/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/Stash/help-url.html b/src/main/resources/hudson/plugins/git/browser/Stash/help-url.html deleted file mode 100644 index d81043b71c..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/Stash/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the HTTP URL for this repository's Stash page (such as http://stash.mydomain.com:7990/projects/PROJECT_KEY/repos/myrepo). -
    diff --git a/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config.jelly b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config.jelly new file mode 100644 index 0000000000..7fb4e78b5c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config_it.properties b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config_it.properties new file mode 100644 index 0000000000..3b3dd67085 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/config_it.properties @@ -0,0 +1 @@ +URL\ or\ name=URL o nome diff --git a/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/help-repoUrl.html new file mode 100644 index 0000000000..ef0ffeb2ab --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/TFS2013GitRepositoryBrowser/help-repoUrl.html @@ -0,0 +1,6 @@ +
    + Either the name of the remote whose URL should be used, or the URL of this + module in TFS (such as http://fisheye6.cenqua.com/tfs/PROJECT/_git/REPO/). + If empty (default), the URL of the "origin" repository is used. +

    If TFS is also used as the repository server, this can usually be left blank.

    +
    diff --git a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config.jelly b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config.jelly index 7b80f9a339..58b925d9be 100644 --- a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config.jelly +++ b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config.jelly @@ -1,8 +1,9 @@ + - - + + - + - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config_it.properties b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config_it.properties new file mode 100644 index 0000000000..82fe9fdad0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/config_it.properties @@ -0,0 +1,2 @@ +ViewGit\ root\ url=Radice di ViewGit +Project\ Name\ in\ ViewGit=Nome di progetto a ViewGit diff --git a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-projectName.html b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-projectName.html index c50dc48f61..f832f44f6a 100644 --- a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-projectName.html +++ b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-projectName.html @@ -1,4 +1,3 @@
    - Specify the name of the project in ViewGit (e.g. scripts, scuttle etc. from http://code.fealdia.org/viewgit/) + Specify the name of the project in ViewGit (e.g. scripts, scuttle etc. from http://code.fealdia.org/viewgit/).
    - diff --git a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-repoUrl.html b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-repoUrl.html new file mode 100644 index 0000000000..d5b5fc80bb --- /dev/null +++ b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-repoUrl.html @@ -0,0 +1,3 @@ +
    + Specify the root URL serving this repository (such as http://code.fealdia.org/viewgit/). +
    diff --git a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-url.html b/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-url.html deleted file mode 100644 index 802b4992dd..0000000000 --- a/src/main/resources/hudson/plugins/git/browser/ViewGitWeb/help-url.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - Specify the root URL serving this repository (such as this). -
    diff --git a/src/main/resources/hudson/plugins/git/extensions/GitSCMExtension/config.groovy b/src/main/resources/hudson/plugins/git/extensions/GitSCMExtension/config.groovy index 1ef75d1f0f..7907fe6dd5 100644 --- a/src/main/resources/hudson/plugins/git/extensions/GitSCMExtension/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/GitSCMExtension/config.groovy @@ -1,3 +1,3 @@ -package hudson.plugins.git.extensions.GitSCMExtension; +package hudson.plugins.git.extensions.GitSCMExtension -// no configuration \ No newline at end of file +// no configuration diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help.html index 7f0e12edca..80008f6249 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help.html @@ -1,4 +1,4 @@
    The default behavior is to use the Git commit's "Committer" value in Jenkins' build changesets. If this option is selected, the Git commit's "Author" value would be used instead. -
    \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help_ja.html new file mode 100644 index 0000000000..a7bc3c3a13 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/AuthorInChangelog/help_ja.html @@ -0,0 +1,7 @@ +
    + デフォルトの振る舞いは、Gitのコミットの"コミッタ"の値をJenkinsのビルドの変更履歴画面で使用します。このオプションを選択すると、 + Gitのコミットの"作者"の値が代わりに使用されます。
    + この方式を使用すると、速いgit ls-remoteによるポーリングシステムの妨げになります。 + そして、Force polling using workspaceを選んだ場合と同様に、 + ポーリングにワークスペースが必須になり、時々不要なビルドを引き起こします。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config.groovy index d34ff6758e..4fd44bf598 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config.groovy @@ -1,8 +1,8 @@ package hudson.plugins.git.extensions.impl.BuildChooserSetting -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry() { f.dropdownDescriptorSelector(title:_("Choosing strategy"), field:"buildChooser", descriptors: descriptor.getBuildChooserDescriptors(my)) -} \ No newline at end of file +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_it.properties b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_it.properties new file mode 100644 index 0000000000..657f1f0d23 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_it.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2017-, Mark Waite +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Choosing\ strategy=Strategia di scelto diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_ja.properties new file mode 100644 index 0000000000..35e27f7b7c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Choosing\ strategy=\u65b9\u5f0f\u306e\u9078\u629e diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/foo.json b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/foo.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/help_ja.html new file mode 100644 index 0000000000..caa5fef51e --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/BuildChooserSetting/help_ja.html @@ -0,0 +1,9 @@ +
    + 1つのジョブで複数のヘッド(たいていは、複数のブランチ)をビルドするなら、 + Jenkinsがどの順番でどのブランチをビルドするのかを選択する方式を、選ぶことができます。 + +

    + この拡張ポイントは、特定のコミットをビルドするジョブを制御するのに、 + 他のたくさんのプラグインに使用されます。これらのプラグインを有効にすると、 + カスタムの方式がここにインストールされます。 +

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/config.groovy new file mode 100644 index 0000000000..4c94fb6ad7 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/config.groovy @@ -0,0 +1,5 @@ +package hudson.plugins.git.extensions.impl.ChangelogToBranch + +def f = namespace(lib.FormTagLib) + +f.property(field:"options") diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help.html new file mode 100644 index 0000000000..8c42fe7227 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help.html @@ -0,0 +1,3 @@ +
    + This method calculates the changelog against the specified branch. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help_ja.html new file mode 100644 index 0000000000..73350dd681 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ChangelogToBranch/help_ja.html @@ -0,0 +1,6 @@ +
    + このメソッドは、指定したブランチの変更履歴を算出します。
    + この拡張動作は、より速いgit ls-remoteコマンドによるポーリングの妨げになり、 + Force polling using workspaceを選択したかのように、ポーリングにワークスペースが必要になり、 + その結果、ときに不要なビルドを引き起こします。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config.groovy new file mode 100644 index 0000000000..9eb4d0b41a --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config.groovy @@ -0,0 +1,7 @@ +package hudson.plugins.git.extensions.impl.CheckoutOption + +def f = namespace(lib.FormTagLib) + +f.entry(title:_("Timeout (in minutes) for checkout operation"), field:"timeout") { + f.number(clazz:"number", min:1, step:1) +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_it.properties b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_it.properties new file mode 100644 index 0000000000..cdca342d6d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_it.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2017-, Mark Waite +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Timeout\ (in\ minutes)\ for\ checkout\ operation=Tempo massimo (a minuti) per operazione checkout diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_ja.properties new file mode 100644 index 0000000000..c7feb2f5d9 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/config_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Timeout\ (in\ minutes)\ for\ checkout\ operation=\ + \u30c1\u30a7\u30c3\u30af\u30a2\u30a6\u30c8\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u5206) \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout.html b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout.html new file mode 100644 index 0000000000..860fc7a392 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout.html @@ -0,0 +1,6 @@ +
    + Specify a timeout (in minutes) for checkout.
    + This option overrides the default timeout of 10 minutes.
    + You can change the global git timeout via the property org.jenkinsci.plugins.gitclient.Git.timeOut (see JENKINS-11286). + Note that property should be set on both master and agent to have effect (see JENKINS-22547). +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout_ja.html new file mode 100644 index 0000000000..8496aff236 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CheckoutOption/help-timeout_ja.html @@ -0,0 +1,8 @@ +
    + チェックアウトのタイムアウト(分)を指定します。 + このオプションは、タイムアウトのデフォルトである10分を上書きします。
    + プロパティ org.jenkinsci.plugins.gitclient.Git.timeOutを設定することで、gitのタイムアウトを変更することができます + (詳細は、JENKINS-11286を参照)。
    + また、プロパティは、マスタとスレーブの両方に設定すると効果がでます + (詳細は、JENKINS-22547を参照)。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/config.groovy new file mode 100644 index 0000000000..8d8d055f45 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/config.groovy @@ -0,0 +1,7 @@ +package hudson.plugins.git.extensions.impl.CleanBeforeCheckout + +def f = namespace(lib.FormTagLib) + +f.entry(title: _("Delete untracked nested repositories"), field: "deleteUntrackedNestedRepositories") { + f.checkbox() +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help-deleteUntrackedNestedRepositories.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help-deleteUntrackedNestedRepositories.html new file mode 100644 index 0000000000..ae971d92be --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help-deleteUntrackedNestedRepositories.html @@ -0,0 +1,3 @@ +
    + Deletes untracked submodules and any other subdirectories which contain .git directories. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help.html index 34e2848560..2e9b479c94 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help.html @@ -1,6 +1,6 @@
    Clean up the workspace before every checkout by deleting all untracked files and directories, - including those which are specified in .gitignore. + including those which are specified in .gitignore. It also resets all tracked files to their versioned state. This ensures that the workspace is in the same state as if you cloned and checked out in a brand-new empty directory, and ensures diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help_ja.html new file mode 100644 index 0000000000..0f7ddf7168 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanBeforeCheckout/help_ja.html @@ -0,0 +1,8 @@ +
    + チェックアウトの前に、追跡していない全てのファイルやディレクトリや、 + .gitignoreで指定されたファイルなどを削除することで、ワークスペースを片付けます。 + すべての追跡しているファイルをリセットして、バージョン管理された状態に戻します。
    + + これは、ワークスペースが、まったく新しい空っぽのディレクトリに、クローンしてチェックアウトしてかのような状態になること、 + そして、以前のビルドが生成したファイルに影響を受けないことを保証します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/config.groovy new file mode 100644 index 0000000000..c54eec196c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/config.groovy @@ -0,0 +1,7 @@ +package hudson.plugins.git.extensions.impl.CleanCheckout + +def f = namespace(lib.FormTagLib) + +f.entry(title: _("Delete untracked nested repositories"), field: "deleteUntrackedNestedRepositories") { + f.checkbox() +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help-deleteUntrackedNestedRepositories.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help-deleteUntrackedNestedRepositories.html new file mode 100644 index 0000000000..ae971d92be --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help-deleteUntrackedNestedRepositories.html @@ -0,0 +1,3 @@ +
    + Deletes untracked submodules and any other subdirectories which contain .git directories. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help.html index dfb415ad5f..b30d0f9649 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help.html @@ -1,6 +1,6 @@
    Clean up the workspace after every checkout by deleting all untracked files and directories, - including those which are specified in .gitignore. + including those which are specified in .gitignore. It also resets all tracked files to their versioned state. This ensures that the workspace is in the same state as if you cloned and checked out in a brand-new empty directory, and ensures diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help_ja.html new file mode 100644 index 0000000000..b8ebef799d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CleanCheckout/help_ja.html @@ -0,0 +1,11 @@ +
    +

    + チェックアウトするごとに、追跡していないファイル、ディレクトリ、 + および.gitignoreに設定されたファイルをすべて削除することで、 + ワークスペースを片付けます。また、追跡しているすべてのファイルをバージョン管理されている状態にリセットします。 + 

    +

    + こうすることで、ワークスペースは、クローンしてまったく新しい空っぽのディレクトリにチェックアウトしたかのような状態であることを保証します。 + また、ビルドが以前のビルドによって生成されたファイルに影響を受けていないことを保証します。 +

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config.groovy index 5df787a437..d41aafb17b 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config.groovy @@ -1,13 +1,22 @@ -package hudson.plugins.git.extensions.impl.CloneOption; +package hudson.plugins.git.extensions.impl.CloneOption -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) +f.entry(title:_("Fetch tags"), field:"noTags") { + f.checkbox(negative:true, checked:(instance==null||!instance.noTags)) +} +f.entry(title:_("Honor refspec on initial clone"), field:"honorRefspec") { + f.checkbox() +} f.entry(title:_("Shallow clone"), field:"shallow") { f.checkbox() } +f.entry(title:_("Shallow clone depth"), field:"depth") { + f.number(clazz:"number", min:1, step:1) +} f.entry(title:_("Path of the reference repo to use during clone"), field:"reference") { f.textbox() } -f.entry(title:_("Timeout (in minutes) for clone and fetch operation"), field:"timeout") { - f.textbox() +f.entry(title:_("Timeout (in minutes) for clone and fetch operations"), field:"timeout") { + f.number(clazz:"number", min:1, step:1) } diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_it.properties b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_it.properties new file mode 100644 index 0000000000..0f8cf6bb7f --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_it.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Shallow\ clone=Clone poco profonde +Shallow\ clone\ depth=Clone profondità +# Do\ not\ fetch\ tags= +# Honor\ refspec\ on\ initial\ clone= +# Path\ of\ the\ reference\ repo\ to\ use\ during\ clone= +Timeout\ (in\ minutes)\ for\ clone\ and\ fetch\ operations=Tempo massimo (a minuti) per clone operazioni diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_ja.properties new file mode 100644 index 0000000000..60a24243a8 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/config_ja.properties @@ -0,0 +1,30 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Shallow\ clone +Shallow\ clone\ depth +Do\ not\ fetch\ tags=\u30bf\u30b0\u3092\u30d5\u30a7\u30c3\u30c1\u3057\u306a\u3044 +Honor\ refspec\ on\ initial\ clone=\u521d\u671f\u30af\u30ed\u30fc\u30f3\u6642\u306erefspec\u3092\u5c0a\u91cd\u3059\u308b +Path\ of\ the\ reference\ repo\ to\ use\ during\ clone=\ + \u30ea\u30d5\u30a1\u30ec\u30f3\u30b9\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30d1\u30b9 +Timeout\ (in\ minutes)\ for\ clone\ and\ fetch\ operations=\ + \u30af\u30ed\u30fc\u30f3\u3068\u30d5\u30a7\u30c3\u30c1\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u5206) diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth.html new file mode 100644 index 0000000000..c89b8367df --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth.html @@ -0,0 +1,4 @@ +
    + Set shallow clone depth, so that git will only download recent history of the project, + saving time and disk space when you just want to access the latest commits of a repository. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth_ja.html new file mode 100644 index 0000000000..bb3abb342d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-depth_ja.html @@ -0,0 +1,4 @@ +
    + gitがプロジェクトの最近の履歴のみをダウンロードするように、shallow cloneの深さを設定することで、 + リポジトリの最新のバージョンにアクセスしたいだけの場合に、時間とディスク容量を節約します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec.html new file mode 100644 index 0000000000..5cb1ebdeca --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec.html @@ -0,0 +1,5 @@ +
    + Perform initial clone using the refspec defined for the repository. + This can save time, data transfer and disk space when you only need + to access the references specified by the refspec. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec_ja.html new file mode 100644 index 0000000000..bb84968d9b --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-honorRefspec_ja.html @@ -0,0 +1,5 @@ +
    + そのリポジトリ用に定義されたrefspecを使用して、最初のcloneを実行します。 + これにより、refspecで指定された参照にアクセスするだけでいい場合に、時間、 + データ転送、およびディスク容量を節約できます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags.html new file mode 100644 index 0000000000..781f42a681 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags.html @@ -0,0 +1,4 @@ +
    + Deselect this to perform a clone without tags, saving time and disk space when you just want to access + what is specified by the refspec. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags_ja.html new file mode 100644 index 0000000000..a953406aa0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-noTags_ja.html @@ -0,0 +1,4 @@ +
    + refspecで指定されたものにアクセスしたいときに、時間とディスク容量を節約するために、 + タグなしでクローンを実行します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference.html index a3be9e85ef..ba1b15ffc9 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference.html @@ -1,4 +1,4 @@
    Specify a folder containing a repository that will be used by Git as a reference during clone operations.
    - This option will be ignored if the folder is not available on the master or slave where the clone is being executed. + This option will be ignored if the folder is not available on the master or agent where the clone is being executed.
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference_ja.html new file mode 100644 index 0000000000..50ef49805a --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-reference_ja.html @@ -0,0 +1,5 @@ +
    + クローン操作中にリファレンスとしてGitに使用されるリポジトリを含むフォルダを指定します。
    + クローンが実行されているときにマスタかスレーブでフォルダが利用できなければ、 + このオプションは無視されます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow.html index f79ec1e645..d9e91d6009 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow.html @@ -1,4 +1,4 @@
    - Perform shallow clone, so that git will not download history of the project, + Perform shallow clone, so that git will not download the history of the project, saving time and disk space when you just want to access the latest version of a repository.
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow_ja.html new file mode 100644 index 0000000000..fa4c8172de --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-shallow_ja.html @@ -0,0 +1,4 @@ +
    + gitがプロジェクトの履歴をダウンロードしないように、shallow cloneを実行することで、 + リポジトリの最新バージョンにのみアクセスしたい場合に、時間とディスク容量を節約します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout.html index e0cf39fcd1..a48bec266e 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout.html @@ -1,5 +1,6 @@
    Specify a timeout (in minutes) for clone and fetch operations.
    This option overrides the default timeout of 10 minutes.
    - You can change the global git timeout via the property org.jenkinsci.plugins.gitclient.Git.timeout (see JENKINS-11286). + You can change the global git timeout via the property org.jenkinsci.plugins.gitclient.Git.timeOut (see JENKINS-11286). + Note that property should be set on both master and agent to have effect (see JENKINS-22547).
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout_ja.html new file mode 100644 index 0000000000..e2d64b52dc --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/CloneOption/help-timeout_ja.html @@ -0,0 +1,7 @@ +
    + クローンとフェッチ操作のタイムアウト(分)を指定します。
    + このオプションは、デフォルトの10分のタイムアウトを上書きします。
    + property org.jenkinsci.plugins.gitclient.Git.timeOutを設定することで、gitのタイムアウトをグローバルに変更することができます + (JENKINS-11286を参照)。
    + プロパティをマスタとスレーブの両方に設定することで、効果があがることに注意してください(JENKINS-22547を参照)。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help.html index 8469286adf..aab2ed979d 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help.html @@ -2,5 +2,5 @@ Git plugin uses git ls-remote polling mechanism by default when configured with a single branch (no wildcards!). This compare the latest built commit SHA with the remote branch without cloning a local copy of the repo.

    If you don't want to / can't use this.

    - If this option is selected, polling will require a workspace and might trigger unwanted builds (see JENKINS-10131). + If this option is selected, polling will require a workspace and might trigger unwanted builds (see JENKINS-10131).
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help_ja.html new file mode 100644 index 0000000000..17a679ec50 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/DisableRemotePoll/help_ja.html @@ -0,0 +1,7 @@ +
    + Gitプラグインは、(ワイルドカードを使わない)1つのブランチが設定されているときは、デフォルトでgit ls-remoteポーリング方式を使用します。 + これにより、リポジトリのローカルコピーを複製せずに、最新ビルドのコミットのSHAとリモートブランチを比較します。

    + + このオプションを選択すると、ポーリングはワークスペースを必要とし、不必要なビルドを引き起こします + (詳細は、JENKINS-10131を参照)。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit/help_ja.html new file mode 100644 index 0000000000..d1793b8a1d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/IgnoreNotifyCommit/help_ja.html @@ -0,0 +1,4 @@ +
    + リポジトリが一致しているかどうかにかかわらず、notifyCommit-URLにアクセスすると、 + このリポジトリは無視されます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config.groovy index 0914daacbc..6074e62c09 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config.groovy @@ -1,6 +1,6 @@ -package hudson.plugins.git.extensions.impl.LocalBranch; +package hudson.plugins.git.extensions.impl.LocalBranch -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Branch name"), field:"localBranch") { f.textbox() diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config_ja.properties new file mode 100644 index 0000000000..a8275657e4 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Branch\ name=\u30d6\u30e9\u30f3\u30c1\u540d diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help.html index c5a89c6b87..dee929072a 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help.html @@ -1,4 +1,11 @@
    If given, checkout the revision to build as HEAD on this branch. +

    + If selected, and its value is an empty string or "**", then the branch + name is computed from the remote branch without the origin. In that + case, a remote branch origin/master will be checked out to a local + branch named master, and a remote branch origin/develop/new-feature + will be checked out to a local branch named develop/newfeature. +

    Please note that this has not been tested with submodules.

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help_ja.html new file mode 100644 index 0000000000..f753a85984 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/LocalBranch/help_ja.html @@ -0,0 +1,10 @@ +
    + リビジョンをチェックアウトして、このブランチのヘッドとしてビルドします。 +

    + 空文字や"**"を設定すると、ブランチ名はoriginを含まないリモートのブランチから算出します。その場合、 + リモートブランチorigin/masterは、ローカルブランチmasterにチェックアウトされ、 + リモートブランチorigin/develop/new-featureは、 + ローカルブランチdevelop/new-featureにチェックアウトされます。 +

    + サブモジュールではテストされていないことに注意してください。 +

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config.groovy new file mode 100644 index 0000000000..780c279fb1 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config.groovy @@ -0,0 +1,7 @@ +package hudson.plugins.git.extensions.impl.MessageExclusion + +def f = namespace(lib.FormTagLib) + +f.entry(title:_("Excluded Messages"), field:"excludedMessage") { + f.textbox() +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config_ja.properties new file mode 100644 index 0000000000..0bd1f74445 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Excluded\ Messages=\u5bfe\u8c61\u5916\u30e1\u30c3\u30bb\u30fc\u30b8 diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage.html b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage.html new file mode 100644 index 0000000000..a9f3b019c0 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage.html @@ -0,0 +1,16 @@ +
    + If set, and Jenkins is set to poll for changes, Jenkins will ignore any revisions committed with message matched to + Pattern when determining + if a build needs to be triggered. This can be used to exclude commits done by the build itself from triggering another build, + assuming the build server commits the change with a distinct message. +

    Exclusion uses Pattern + matching +

    +

    .*\[maven-release-plugin\].*
    + The example above illustrates that if only revisions with "[maven-release-plugin]" message in first comment line + have been committed to the SCM a build will not occur. +

    + You can create more complex patterns using embedded flag expressions. +

    (?s).*FOO.*
    + This example will search FOO message in all comment lines. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage_ja.html new file mode 100644 index 0000000000..8390020a7c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/MessageExclusion/help-excludedMessage_ja.html @@ -0,0 +1,20 @@ +
    + Jenkinsは、変更をポーリングするように設定され、ビルドするかどうか決定する時に、 + パターンに合致するメッセージでコミットされた、リビジョンを無視します。 + これは、ビルドサーバが別のメッセージで変更をコミットすると仮定して、 + ビルド自体が行ったコミットが、別のビルドを引き起こさないように使われます。 + +

    + 対象外メッセージは、パターン + マッチングを使用する。 +

    + +

    .*\[maven-release-plugin\].*
    + 上記に示す例は、コメントの最初の行に"[maven-release-plugin]"メッセージとリビジョンだけが、 + SCMにコミットされた場合、ビルドは行われません。 +

    + 埋め込みフラグを使用して、より複雑なパターンを作ることもできます。 + +

    (?s).*FOO.*
    + この例は、コメント行すべてから、"FOO"を検索します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/Messages.properties b/src/main/resources/hudson/plugins/git/extensions/impl/Messages.properties new file mode 100644 index 0000000000..3405529ba7 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/Messages.properties @@ -0,0 +1 @@ +Advanced.clone.behaviours=Advanced clone behaviours diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config.groovy index 45d2b84c94..0abe54d692 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config.groovy @@ -1,10 +1,10 @@ -package hudson.plugins.git.extensions.impl.PathRestriction; +package hudson.plugins.git.extensions.impl.PathRestriction -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Included Regions"), field:"includedRegions") { f.textarea() } f.entry(title:_("Excluded Regions"), field:"excludedRegions") { f.textarea() -} \ No newline at end of file +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config_ja.properties new file mode 100644 index 0000000000..03e5a98bd2 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/config_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Included\ Regions=\u5bfe\u8c61\u7bc4\u56f2 +Excluded\ Regions=\u5bfe\u8c61\u5916\u7bc4\u56f2 diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions.html index 5ad43e9574..8280f3827e 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions.html @@ -1,17 +1,12 @@
    - If set, and Jenkins is set to poll for changes, Jenkins will ignore any files and/or - folders in this list when determining if a build needs to be triggered. -

    - Each exclusion uses regular expression pattern matching, and must be separated by a new line. + Each exclusion uses java regular expression pattern matching, + and must be separated by a new line.

    -	myapp/src/main/web/.*\.html
    -	myapp/src/main/web/.*\.jpeg
    -	myapp/src/main/web/.*\.gif
    +    myapp/src/main/web/.*\.html
    +    myapp/src/main/web/.*\.jpeg
    +    myapp/src/main/web/.*\.gif
       
    The example above illustrates that if only html/jpeg/gif files have been committed to the SCM a build will not occur. -

    - More information on regular expressions can be found - here.

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions_ja.html new file mode 100644 index 0000000000..a798d0f806 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-excludedRegions_ja.html @@ -0,0 +1,11 @@ +
    + 対象外範囲は、Javaの正規表現のパターンマッチングを使用し、 + 改行で区切られています。 +

    +

    +    myapp/src/main/web/.*\.html
    +    myapp/src/main/web/.*\.jpeg
    +    myapp/src/main/web/.*\.gif
    +  
    + 上記の例は、html/jpeg/gifファイルがSCMにコミットされた場合のみ、ビルドが行われないことを示しています。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions.html index 49372e3ab4..1436c49156 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions.html @@ -1,19 +1,14 @@
    - If set, and Jenkins is set to poll for changes, Jenkins will honor any files and/or - folders in this list when determining if a build needs to be triggered. -

    - Each inclusion uses regular expression pattern matching, and must be separated by a new line. + Each inclusion uses java regular expression pattern matching, + and must be separated by a new line. An empty list implies that everything is included.

    -	myapp/src/main/web/.*\.html
    -	myapp/src/main/web/.*\.jpeg
    -	myapp/src/main/web/.*\.gif
    +    myapp/src/main/web/.*\.html
    +    myapp/src/main/web/.*\.jpeg
    +    myapp/src/main/web/.*\.gif
       
    The example above illustrates that a build will only occur, if html/jpeg/gif files have been committed to the SCM. Exclusions take precedence over inclusions, if there is an overlap between included and excluded regions. -

    - More information on regular expressions can be found - here.

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions_ja.html new file mode 100644 index 0000000000..44ccd6ef97 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help-includedRegions_ja.html @@ -0,0 +1,13 @@ +
    + 対象範囲は、Javaの正規表現パターンマッチングを使用し、改行で区切られています。 + 空行は、すべてを含むことを意味します。 +

    +

    +    myapp/src/main/web/.*\.html
    +    myapp/src/main/web/.*\.jpeg
    +    myapp/src/main/web/.*\.gif
    +  
    + 上記の例は、html/jpeg/gifファイルがSCMにコミットされた場合のみ、 + ビルドが行われることを示しています。 + 対象範囲と対象外範囲の間に重複がある場合、対象外範囲が対象範囲よりも優先されます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help.html new file mode 100644 index 0000000000..860a4ef765 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help.html @@ -0,0 +1,6 @@ +
    + If set, and Jenkins is set to poll for changes, Jenkins will pay attention to included and/or excluded files and/or + folders when determining if a build needs to be triggered. +

    + Using this behaviour will preclude the faster git ls-remote polling mechanism, forcing polling to require a workspace thus sometimes triggering unwanted builds, as if you had selected the Force polling using workspace extension as well. +

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help_ja.html new file mode 100644 index 0000000000..0cb98a6cd3 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PathRestriction/help_ja.html @@ -0,0 +1,8 @@ +
    + Jenkinsは変更をポーリングするように設定され、ビルドを行うかどうか判断する時に、 + ファイルやフォルダを含むか、含まないかに注意を払います。 +

    + この処理を使用すると、速いgit ls-remoteを使ったポーリングシステムの妨げになります。 + そして、Force polling using workspaceを選択したかのように、 + ポーリングにはワークスペースを必要とし、ときどき、不要なビルドを引き起こすこともあります。 +

    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PerBuildTag/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PerBuildTag/help_ja.html new file mode 100644 index 0000000000..4d0afad3ab --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PerBuildTag/help_ja.html @@ -0,0 +1,4 @@ +
    + ビルド毎にワークスペースにタグを作成して、ビルドされたコミットに印をつけます。 + Git Publisherと組み合わせて、リモートリポジトリにそのタグをプッシュすることができます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/config.groovy index 063593b1bd..efda23021b 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/config.groovy @@ -1,5 +1,5 @@ -package hudson.plugins.git.extensions.impl.PreBuildMerge; +package hudson.plugins.git.extensions.impl.PreBuildMerge -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.property(field:"options") diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help.html index 9ef2839ec3..7b66721bc0 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help.html @@ -1,7 +1,7 @@
    - These options allow you to perform a merge to a particular branch before building. - For example, you could specify an integration branch to be built, and to merge to master. - In this scenario, on every change of integration, Jenkins will perform a merge with the master branch, + These options allow you to perform a merge to a particular branch before building. + For example, you could specify an integration branch to be built, and to merge to master. + In this scenario, on every change of integration, Jenkins will perform a merge with the master branch, and try to perform a build if the merge is successful. It then may push the merge back to the remote repository if the Git Push post-build action is selected. -
    \ No newline at end of file +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help_ja.html new file mode 100644 index 0000000000..d5f07f061c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PreBuildMerge/help_ja.html @@ -0,0 +1,12 @@ +
    +

    + これらのオプションを使うことで、ビルドする前に特定のブランチにマージを実行することができます。 + 例えば、ビルドするインテグレーションブランチを指定することができますし、 + マスタにマージすることも指定できます。 +

    +

    + このシナリオでは、インテグレーションの変更ごとに、Jenkinsはマスタブランチとマージを実行します。 + そして、マージが成功したら、ビルドを実行しようとします。 + そして、ビルド後の処理でGit Publisherが選択されていれば、マージした結果をリモートのリポジトリにプッシュするでしょう。 +

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PruneStaleBranch/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/PruneStaleBranch/help_ja.html new file mode 100644 index 0000000000..1561866f1b --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PruneStaleBranch/help_ja.html @@ -0,0 +1,3 @@ +
    + "git remote prune"を各リモートごとに起動し、手元の使用されていないローカルブランチを削除します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config.groovy index 4daa3d373a..93a75241a6 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config.groovy @@ -1,6 +1,6 @@ -package hudson.plugins.git.extensions.impl.RelativeTargetDirectory; +package hudson.plugins.git.extensions.impl.RelativeTargetDirectory -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Local subdirectory for repo"), field:"relativeTargetDir") { f.textbox() diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config_ja.properties new file mode 100644 index 0000000000..af6bc86955 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Local\ subdirectory\ for\ repo=\u30b5\u30d6\u30c7\u30a3\u30ec\u30af\u30c8\u30ea diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir.html b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir.html index 965eb98af5..2d9a7de5c6 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir.html @@ -1,5 +1,5 @@
    - Specify a local directory (relative to the workspace root) + Specify a local directory (relative to the workspace root) where the Git repository will be checked out. If left empty, the workspace root itself will be used.
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir_ja.html new file mode 100644 index 0000000000..8a645ba7f8 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/RelativeTargetDirectory/help-relativeTargetDir_ja.html @@ -0,0 +1,4 @@ +
    + Gitリポジトリがチェックアウトされるローカルなディレクトリ(ワークスペースのルートからの相対パス)を指定します。 + 指定しない場合、ワークスペースのルートを使用します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config.groovy index 0f3b347083..9745899411 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config.groovy @@ -1,6 +1,6 @@ -package hudson.plugins.git.extensions.impl.ScmName; +package hudson.plugins.git.extensions.impl.ScmName -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Unique SCM name"), field:"name") { f.textbox() diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config_ja.properties new file mode 100644 index 0000000000..51595af542 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Unique\ SCM\ name=SCM\u306e\u540d\u524d diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/help_ja.html new file mode 100644 index 0000000000..9cd09ab80a --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/ScmName/help_ja.html @@ -0,0 +1,3 @@ +
    +

    このSCMのユニークな名前です。Multi SCMプラグインでGitを使用する場合に必要です。

    +
    \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config.jelly b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config.jelly index 12e46c0c89..6ddce6a26e 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config.jelly +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config.jelly @@ -1,14 +1,15 @@ + - +
    - +
    -
    \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_it.properties b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_it.properties new file mode 100644 index 0000000000..bc239aca15 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_it.properties @@ -0,0 +1,2 @@ +Path=Percorso del file +Delete\ Path=Rimuovi percorso del file diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_ja.properties new file mode 100644 index 0000000000..d7197db6d3 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPath/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Path=\u30d1\u30b9 diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/config.jelly b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/config.jelly index de15f4f0e9..9f6d665637 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/config.jelly +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/config.jelly @@ -1,12 +1,8 @@ + - - - - \ No newline at end of file + diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/help_ja.html new file mode 100644 index 0000000000..8809e87e20 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SparseCheckoutPaths/help_ja.html @@ -0,0 +1,6 @@ +
    +

    + sparse checkoutしたいパスを指定します。これはディスクスペースを節約するために使用することができます(リファレンスリポジトリを思い出してください)。 + 少なくとも1.7.10より新しいバージョンのGitを必ず使用してください。 +

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config.groovy index 0e232f5ca8..1e0d08ccec 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config.groovy @@ -1,6 +1,6 @@ -package hudson.plugins.git.extensions.impl.SubmoduleOption; +package hudson.plugins.git.extensions.impl.SubmoduleOption -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Disable submodules processing"), field:"disableSubmodules") { f.checkbox() @@ -11,6 +11,24 @@ f.entry(title:_("Recursively update submodules"), field:"recursiveSubmodules") { f.entry(title:_("Update tracking submodules to tip of branch"), field:"trackingSubmodules") { f.checkbox() } +f.entry(title:_("Use credentials from default remote of parent repository"), field:"parentCredentials") { + f.checkbox() +} +f.entry(title:_("Shallow clone"), field:"shallow") { + f.checkbox() +} +f.entry(title:_("Shallow clone depth"), field:"depth") { + f.number(clazz:"number", min:1, step:1) +} +f.entry(title:_("Path of the reference repo to use during submodule update"), field:"reference") { + f.textbox() +} +f.entry(title:_("Timeout (in minutes) for submodules operations"), field:"timeout") { + f.number(clazz:"number", min:1, step:1) +} +f.entry(title:_("Number of threads to use when updating submodules"), field:"threads") { + f.number(clazz:"number", min:1, step:1) +} /* This needs more thought diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config_ja.properties new file mode 100644 index 0000000000..65d479eaaf --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/config_ja.properties @@ -0,0 +1,32 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Disable\ submodules\ processing=\u30b5\u30d6\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u51e6\u7406\u3092\u7121\u52b9\u5316 +Recursively\ update\ submodules=\u30b5\u30d6\u30e2\u30b8\u30e5\u30fc\u30eb\u3092\u518d\u5e30\u7684\u306b\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 +Update\ tracking\ submodules\ to\ tip\ of\ branch=\ + \u30b5\u30d6\u30e2\u30b8\u30e5\u30fc\u30eb\u3092\u30d6\u30e9\u30f3\u30c1\u306e\u5148\u982d\u306b\u66f4\u65b0 +Path\ of\ the\ reference\ repo\ to\ use\ during\ submodule\ update=\ + \u30ea\u30d5\u30a1\u30ec\u30f3\u30b9\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30d1\u30b9 +Use\ credentials\ from\ default\ remote\ of\ parent\ repository=\ + \u89aa\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30ea\u30e2\u30fc\u30c8\u304b\u3089\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u4f7f\u7528 +Timeout\ (in\ minutes)\ for\ submodules\ operations=\ + \u30b5\u30d6\u30e2\u30b8\u30e5\u30fc\u30eb\u64cd\u4f5c\u3067\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8(\u5206) diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth.html new file mode 100644 index 0000000000..c89b8367df --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth.html @@ -0,0 +1,4 @@ +
    + Set shallow clone depth, so that git will only download recent history of the project, + saving time and disk space when you just want to access the latest commits of a repository. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth_ja.html new file mode 100644 index 0000000000..bb3abb342d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-depth_ja.html @@ -0,0 +1,4 @@ +
    + gitがプロジェクトの最近の履歴のみをダウンロードするように、shallow cloneの深さを設定することで、 + リポジトリの最新のバージョンにアクセスしたいだけの場合に、時間とディスク容量を節約します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-disableSubmodules_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-disableSubmodules_ja.html new file mode 100644 index 0000000000..d1a67b43ed --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-disableSubmodules_ja.html @@ -0,0 +1,4 @@ +
    + サブモジュールへのサポートを無効にしても、Gitプラグインの基本的な機能を使い続けることができるので、 + 最初から存在しないかのように、Jenkinsにサブモジュールを完璧に無視させます。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials.html new file mode 100644 index 0000000000..7d3c109969 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials.html @@ -0,0 +1,3 @@ +
    + Use credentials from the default remote of the parent project. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials_ja.html new file mode 100644 index 0000000000..9a0bc6debb --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-parentCredentials_ja.html @@ -0,0 +1,3 @@ +
    + 親プロジェクトのデフォルトのリモートからの認証情報を使用します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules.html index e747c69a9b..7ccd89a9cb 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules.html @@ -1,5 +1,5 @@
    - Retrieve all submodules recursively - - (uses '--recursive' option which requires git>=1.6.5) + Retrieve all submodules recursively + + (uses '--recursive' option which requires git>=1.6.5)
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules_ja.html new file mode 100644 index 0000000000..d15915b0dd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-recursiveSubmodules_ja.html @@ -0,0 +1,4 @@ +
    + 再帰的に、すべてのサブモジュールを取得します。
    + (git >= 1.6.5で、'--recursive'オプションを使用します) +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference.html new file mode 100644 index 0000000000..b055aa2afd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference.html @@ -0,0 +1,11 @@ +
    + Specify a folder containing a repository that will be used by Git as a reference during clone operations.
    + This option will be ignored if the folder is not available on the master or agent where the clone is being executed.
    + To prepare a reference folder with multiple subprojects, create a bare git repository and add all the remote urls then perform a fetch:
    +
    +  git init --bare
    +  git remote add SubProject1 https://gitrepo.com/subproject1
    +  git remote add SubProject2 https://gitrepo.com/subproject2
    +  git fetch --all
    +  
    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference_ja.html new file mode 100644 index 0000000000..60d5e9106f --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-reference_ja.html @@ -0,0 +1,12 @@ +
    + クローン中にリファレンスとしてGitが使用するリポジトリを含むパスを指定してください。
    + クローンが行われるマスタかスレーブ上で、そのパスを利用できない場合、このオプションは無視されます。
    + 複数サブプロジェクトを持つリファレンスを用意するには、ベアリポジトリを作成して、 + リモートのURLを追加して、フェッチします。
    +
    +  git init --bare
    +  git remote add SubProject1 https://gitrepo.com/subproject1
    +  git remote add SubProject2 https://gitrepo.com/subproject2
    +  git fetch --all
    +  
    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow.html new file mode 100644 index 0000000000..d9e91d6009 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow.html @@ -0,0 +1,4 @@ +
    + Perform shallow clone, so that git will not download the history of the project, + saving time and disk space when you just want to access the latest version of a repository. +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow_ja.html new file mode 100644 index 0000000000..fa4c8172de --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-shallow_ja.html @@ -0,0 +1,4 @@ +
    + gitがプロジェクトの履歴をダウンロードしないように、shallow cloneを実行することで、 + リポジトリの最新バージョンにのみアクセスしたい場合に、時間とディスク容量を節約します。 +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout.html new file mode 100644 index 0000000000..7fc07aafec --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout.html @@ -0,0 +1,6 @@ +
    + Specify a timeout (in minutes) for submodules operations.
    + This option overrides the default timeout of 10 minutes.
    + You can change the global git timeout via the property org.jenkinsci.plugins.gitclient.Git.timeOut (see JENKINS-11286). + Note that property should be set on both master and agent to have effect (see JENKINS-22547). +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout_ja.html new file mode 100644 index 0000000000..ef76c42296 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-timeout_ja.html @@ -0,0 +1,7 @@ +
    + サブモジュールの操作のタイムアウト(分)を指定します。このオプションは、デフォルトの10分を上書きします。 + プロパティorg.jenkinsci.plugins.gitclient.Git.timeOutを使用して、gitのグローバルなタイムアウトを変更することができます。
    + (詳細は、JENKINS-11286を参照してください。)
    + 注意:プロパティは、効果がでるように、マスタとスレーブの両方に設定すべきです。
    + (詳細は、JENKINS-22547を参照してください。) +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-trackingSubmodules_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-trackingSubmodules_ja.html new file mode 100644 index 0000000000..dd43e4aafb --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/SubmoduleOption/help-trackingSubmodules_ja.html @@ -0,0 +1,4 @@ +
    + .gitmodulesに設定されたブランチの先頭を取得します。
    + (git>1.8.2で、'--remote'オプションを使用します。) +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config.groovy index 9e332975a9..a93a7f690a 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config.groovy @@ -1,7 +1,7 @@ -package hudson.plugins.git.extensions.impl.UserExclusion; +package hudson.plugins.git.extensions.impl.UserExclusion -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("Excluded Users"), field:"excludedUsers") { f.textarea() -} \ No newline at end of file +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config_ja.properties b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config_ja.properties new file mode 100644 index 0000000000..0c11f739dd --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/config_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Excluded\ Users=\u5bfe\u8c61\u5916\u30e6\u30fc\u30b6\u30fc diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers.html b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers.html index 339d40655a..f78d3b08c8 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers.html +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers.html @@ -1,9 +1,9 @@
    If set, and Jenkins is set to poll for changes, Jenkins will ignore any revisions committed by users in this list when determining if a build needs to be triggered. This can be used to exclude commits done by the build itself from triggering another build, assuming the build server commits the change with a distinct SCM user. +

    + Using this behaviour will preclude the faster git ls-remote polling mechanism, forcing polling to require a workspace thus sometimes triggering unwanted builds, as if you had selected the Force polling using workspace extension as well.

    Each exclusion uses literal pattern matching, and must be separated by a new line.

    -

    -	 auto_build_user
    -  
    +
    auto_build_user
    The example above illustrates that if only revisions by "auto_build_user" have been committed to the SCM a build will not occur.
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers_ja.html new file mode 100644 index 0000000000..d3dee5891b --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserExclusion/help-excludedUsers_ja.html @@ -0,0 +1,20 @@ +
    +

    + Jenkinsが変更をポーリングするように設定されると、ビルドを実行する必要があるかどうか決定するときに、 + Jenkinsはこのリストにあるユーザーによるコミットを無視します。これは、ビルド自身が行ったコミットが、 + 別のビルドを引き起こすのを避けるために使用できます。ビルドサーバが変更を異なるSCMユーザーでコミットすると仮定する場合ですが。 +

    +

    + この機能は、より速いgit ls-remoteを使用したポーリングの仕組みを妨げます。 + そして、Force polling using workspaceを選択したかのように、ポーリングにワークスペースが必要になり、 + 時々不要なビルドを引き起こします。 +

    +

    + 対象外ユーザーは、文字通りパターンマッチングを使用して、改行で分離されなければなりません。 +

    + +
    auto_build_user
    +

    + "auto_build_user"によって、SCMにコミットされたリビジョンだけは、ビルドは起動しません。 +

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/config.groovy index 631bfa246f..ff00ba8d72 100644 --- a/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/config.groovy +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/config.groovy @@ -1,6 +1,6 @@ -package hudson.plugins.git.extensions.impl.UserIdentity; +package hudson.plugins.git.extensions.impl.UserIdentity -def f = namespace(lib.FormTagLib); +def f = namespace(lib.FormTagLib) f.entry(title:_("user.name"), field:"name") { f.textbox() diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-email_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-email_ja.html new file mode 100644 index 0000000000..1299b37319 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-email_ja.html @@ -0,0 +1,4 @@ +
    +

    ビルドの前に"git config user.email [this]"が呼ばれます。 + これは、システムの設定のグローバルな設定値を上書きします。

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-name_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-name_ja.html new file mode 100644 index 0000000000..b7caeb90e1 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/UserIdentity/help-name_ja.html @@ -0,0 +1,4 @@ +
    +

    ビルドの前に"git config user.name [this]"が呼ばれます。 + これは、システムの設定のグローバルな設定値を上書きします。

    +
    diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/WipeWorkspace/help_ja.html b/src/main/resources/hudson/plugins/git/extensions/impl/WipeWorkspace/help_ja.html new file mode 100644 index 0000000000..4b42fb9e17 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/WipeWorkspace/help_ja.html @@ -0,0 +1,3 @@ +
    + 完全に新しいワークスペースを提供するために、ビルドの前にワークスペースの内容を削除します。 +
    diff --git a/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.groovy b/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.groovy new file mode 100644 index 0000000000..3ed2a98f4c --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.groovy @@ -0,0 +1,19 @@ +package hudson.plugins.git.util.AncestryBuildChooser + +def f = namespace(lib.FormTagLib) + +f.description { + raw(_("maximum_age_of_commit_blurb")) +} + +f.entry(title:_("Maximum Age of Commit"), field:"maximumAgeInDays") { + f.number(clazz:"number", min:0, step:1) +} + +f.description { + raw(_("commit_in_ancestry_blurb")) +} + +f.entry(title:_("Commit in Ancestry"), field:"ancestorCommitSha1") { + f.textbox() +} diff --git a/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.properties b/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.properties new file mode 100644 index 0000000000..930c91f49d --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/AncestryBuildChooser/config.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +maximum_age_of_commit_blurb=The maximum age of a commit (in days) for it to be built. This uses the GIT_COMMITTER_DATE, not GIT_AUTHOR_DATE. +commit_in_ancestry_blurb=If an ancestor commit (sha1) is provided, only branches with this commit in their history will be built. \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/util/BuildChooser/config.groovy b/src/main/resources/hudson/plugins/git/util/BuildChooser/config.groovy index cdbc31409a..736c2c3c62 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildChooser/config.groovy +++ b/src/main/resources/hudson/plugins/git/util/BuildChooser/config.groovy @@ -1,3 +1,3 @@ -package hudson.plugins.git.util.BuildChooser; +package hudson.plugins.git.util.BuildChooser // no configuration diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly b/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly index 01dbec9cdc..36dfeb6fd8 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly @@ -1,19 +1,22 @@ + - - - -

    Git Build Data

    + + + + + +

    ${%Git Build Data}

    - Revision: ${it.lastBuild.SHA1.name()} - from SCM: ${it.scmName} + ${%Revision}: ${it.lastBuild.SHA1.name()} + from SCM: ${it.scmName}
    • ${branch.name}
    -

    Built Branches

    +

    ${%Built Branches}

      diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/index_it.properties b/src/main/resources/hudson/plugins/git/util/BuildData/index_it.properties new file mode 100644 index 0000000000..bec44ded59 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/BuildData/index_it.properties @@ -0,0 +1,3 @@ +Git\ Build\ Data=Dati del progetto git +Revision=Revisione +Built\ Branches=Rami costruitti diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/index_ja.properties b/src/main/resources/hudson/plugins/git/util/BuildData/index_ja.properties new file mode 100644 index 0000000000..06b9de3be3 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/BuildData/index_ja.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Git\ Build\ Data=Git\u30d3\u30eb\u30c9\u30c7\u30fc\u30bf +Revision=\u30ea\u30d3\u30b8\u30e7\u30f3 +Built\ Branches=\u30d3\u30eb\u30c9\u3057\u305f\u30d6\u30e9\u30f3\u30c1 diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly index 3706b6687a..853a96909b 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly @@ -1,10 +1,11 @@ + - Revision: ${it.lastBuiltRevision.sha1.name()} - from SCM: ${it.scmName} + ${%Revision}: ${it.lastBuiltRevision.sha1.name()} + from SCM: ${it.scmName}
        diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/summary_it.properties b/src/main/resources/hudson/plugins/git/util/BuildData/summary_it.properties new file mode 100644 index 0000000000..084290bba7 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary_it.properties @@ -0,0 +1 @@ +Revision=Revisione diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/summary_ja.properties b/src/main/resources/hudson/plugins/git/util/BuildData/summary_ja.properties new file mode 100644 index 0000000000..30117f8d06 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary_ja.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Revision=\u30ea\u30d3\u30b8\u30e7\u30f3 diff --git a/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.groovy b/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.groovy index 94741c4e02..94ae52bbbf 100644 --- a/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.groovy +++ b/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.groovy @@ -1,4 +1,4 @@ -package hudson.plugins.git.util.InverseBuildChooser; +package hudson.plugins.git.util.InverseBuildChooser def f = namespace(lib.FormTagLib) diff --git a/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.properties b/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.properties index 2ab1b7f4ca..ec6dece54d 100644 --- a/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.properties +++ b/src/main/resources/hudson/plugins/git/util/InverseBuildChooser/config.properties @@ -1,5 +1,5 @@ -blurb=Build all branches except for those which match the branch specifiers configure above. \ - This is useful, for example, when you have jobs building your master and various \ - release branches and you want a second job which builds all new feature branches — \ +blurb=Build all branches except for those which match the branch specifiers configure above. \ + This is useful, for example, when you have jobs building your master and various \ + release branches and you want a second job which builds all new feature branches — \ i.e. branches which do not match these patterns — without redundantly building \ - master and the release branches again each time they change. \ No newline at end of file + master and the release branches again each time they change. \ No newline at end of file diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 815fefc339..0563a08019 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -1,3 +1,4 @@ +
        - This plugin integrates GIT with Jenkins. -
        \ No newline at end of file + This plugin integrates Git with Jenkins. + diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail.jelly b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail.jelly index c82a781ffc..2c4618f3ad 100644 --- a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail.jelly +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail.jelly @@ -1,3 +1,4 @@ + - + - - + + - - - - - - - - diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en.properties b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en.properties new file mode 100644 index 0000000000..e337e9c00f --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en.properties @@ -0,0 +1 @@ +Behaviours=Behaviours diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en_US.properties b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en_US.properties new file mode 100644 index 0000000000..4155e0949a --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_en_US.properties @@ -0,0 +1 @@ +Behaviours=Behaviors diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_it.properties b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_it.properties new file mode 100644 index 0000000000..fce40e793d --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_it.properties @@ -0,0 +1,2 @@ +Project\ Repository=Deposito del progetto +Credentials=Credenziali diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_ja.properties b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_ja.properties new file mode 100644 index 0000000000..965b032844 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/config-detail_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Project\ Repository=\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30ea\u30dd\u30b8\u30c8\u30ea +Credentials=\u8a8d\u8a3c\u60c5\u5831 diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/help-credentialsId.html b/src/main/resources/jenkins/plugins/git/GitSCMSource/help-credentialsId.html new file mode 100644 index 0000000000..b735699d43 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/help-credentialsId.html @@ -0,0 +1,27 @@ + + +
        + Credentials used to scan branches and check out sources. +
        diff --git a/src/main/resources/jenkins/plugins/git/GitSCMSource/help-remote.html b/src/main/resources/jenkins/plugins/git/GitSCMSource/help-remote.html new file mode 100644 index 0000000000..558b095b5a --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitSCMSource/help-remote.html @@ -0,0 +1,27 @@ + + +
        + Specify the URL of this remote repository. This uses the same syntax as your git clone command. +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/config.jelly b/src/main/resources/jenkins/plugins/git/GitStep/config.jelly new file mode 100644 index 0000000000..d8d2ca0e48 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/config.jelly @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/GitStep/config_it.properties b/src/main/resources/jenkins/plugins/git/GitStep/config_it.properties new file mode 100644 index 0000000000..978ec2e12d --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/config_it.properties @@ -0,0 +1,3 @@ +Repository\ URL=URL deposito +Branch=Ramo +Credentials=Credenziali diff --git a/src/main/resources/jenkins/plugins/git/GitStep/config_ja.properties b/src/main/resources/jenkins/plugins/git/GitStep/config_ja.properties new file mode 100644 index 0000000000..32cc60598c --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/config_ja.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Repository\ URL=\u30ea\u30dd\u30b8\u30c8\u30eaURL +Branch=\u30d6\u30e9\u30f3\u30c1 +Credentials=\u8a8d\u8a3c\u60c5\u5831 diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help-branch.html b/src/main/resources/jenkins/plugins/git/GitStep/help-branch.html new file mode 100644 index 0000000000..0a3a36d185 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help-branch.html @@ -0,0 +1,11 @@ +
        +

        + Branch to be checked out in the workspace. Default is the remote repository's default branch (typically 'master'). +

        +

        + Note that this must be a local branch name like 'master' or 'develop' or a tag name. + Remote branch names like 'origin/master' and 'origin/develop' are not supported as the branch argument. + SHA-1 hashes are not supported as the branch argument. + Remote branch names and SHA-1 hashes are supported by the general purpose checkout step. +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help-changelog.html b/src/main/resources/jenkins/plugins/git/GitStep/help-changelog.html new file mode 100644 index 0000000000..792fd92a56 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help-changelog.html @@ -0,0 +1,9 @@ +
        +

        + Compute changelog for this job. Default is 'true'. +

        +

        + If changelog is false, then the changelog will not be computed for this job. + If changelog is true or is not set, then the changelog will be computed. +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help-credentialsId.html b/src/main/resources/jenkins/plugins/git/GitStep/help-credentialsId.html new file mode 100644 index 0000000000..bde0db8fe1 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help-credentialsId.html @@ -0,0 +1,9 @@ +
        +

        + Identifier of the credential used to access the remote git repository. Default is '<empty>'. +

        +

        + The credential must be a private key credential if the remote git repository is accessed with the ssh protocol. + The credential must be a username / password credential if the remote git repository is accessed with http or https protocol. +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help-poll.html b/src/main/resources/jenkins/plugins/git/GitStep/help-poll.html new file mode 100644 index 0000000000..bfad205169 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help-poll.html @@ -0,0 +1,9 @@ +
        +

        + Poll remote repository for changes. Default is 'true'. +

        +

        + If poll is false, then the remote repository will not be polled for changes. + If poll is true or is not set, then the remote repository will be polled for changes. +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help-url.html b/src/main/resources/jenkins/plugins/git/GitStep/help-url.html new file mode 100644 index 0000000000..2cd33420f6 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help-url.html @@ -0,0 +1,10 @@ +
        +

        + URL of the repository to be checked out in the workspace. Required parameter. +

        +

        + Repository URL's should follow the git URL guidelines. + Git steps to access a secured repository should provide a Jenkins credential with the credentialsId argument rather than embedding credentials in the URL. + Credentials embedded in a repository URL may be visible in console logs or in other log files. +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help.html b/src/main/resources/jenkins/plugins/git/GitStep/help.html new file mode 100644 index 0000000000..aad0094689 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help.html @@ -0,0 +1,117 @@ +
        +

        + Git step. It performs a clone from the specified repository. +

        +

        + Use the Pipeline Snippet Generator to generate a sample pipeline script for the git step. + More advanced checkout operations require the checkout step rather than the git step. + Examples of the git step include: +

        +

        + +

        + The git step is a simplified shorthand for a subset of the more powerful checkout step: +

        +checkout([$class: 'GitSCM', branches: [[name: '*/master']],
        +    userRemoteConfigs: [[url: 'http://git-server/user/repository.git']]])
        +
        +

        + +
        + +

        + NOTE: The checkout step is the preferred SCM checkout method. + It provides significantly more functionality than the git step. +

        +

        Use the Pipeline Snippet Generator to generate a sample pipeline script for the checkout step. +

        +

        + The checkout step can be used in many cases where the git step cannot be used. + Refer to the git plugin documentation for detailed descriptions of options available to the checkout step. + For example, the git step does not support: +

          +
        • SHA-1 checkout
        • +
        • Tag checkout
        • +
        • Submodule checkout
        • +
        • Sparse checkout
        • +
        • Large file checkout (LFS)
        • +
        • Reference repositories
        • +
        • Branch merges
        • +
        • Repository tagging
        • +
        • Custom refspecs
        • +
        • Timeout configuration
        • +
        • Changelog calculation against a non-default reference
        • +
        • Stale branch pruning
        • +
        +

        + +
        + + Example: Git step with defaults +

        + Checkout from the git plugin source repository using https protocol, no credentials, and the master branch. +

        The Pipeline Snippet Generator generates this example: +

        +git 'https://github.com/jenkinsci/git-plugin'
        +
        +

        + + Example: Git step with https and a specific branch +

        + Checkout from the Jenkins source repository using https protocol, no credentials, and a specific branch (stable-2.204). + Note that this must be a local branch name like 'master' or 'develop' or a tag name. + Remote branch names like 'origin/master' and 'origin/develop' are not supported as the branch argument. + SHA-1 hashes are not supported as the branch argument. + Remote branch names and SHA-1 hashes are supported by the general purpose checkout step. +

        The Pipeline Snippet Generator generates this example: +

        +git branch: 'stable-2.204',
        +    url: 'https://github.com/jenkinsci/jenkins.git'
        +
        +

        + + Example: Git step with ssh and a private key credential +

        + Checkout from the git client plugin source repository using ssh protocol, private key credentials, and the master branch. + The credential must be a private key credential if the remote git repository is accessed with the ssh protocol. + The credential must be a username / password credential if the remote git repository is accessed with http or https protocol. +

        The Pipeline Snippet Generator generates this example: +

        +git credentialsId: 'my-private-key-credential-id',
        +    url: 'git@github.com:jenkinsci/git-client-plugin.git'
        +
        +

        + + Example: Git step with https and changelog disabled +

        + Checkout from the Jenkins source repository using https protocol, no credentials, the master branch, and changelog calculation disabled. + If changelog is false, then the changelog will not be computed for this job. + If changelog is true or is not set, then the changelog will be computed. + See the workflow scm step documentation for more changelog details. +

        The Pipeline Snippet Generator generates this example: +

        +git changelog: false,
        +    url: 'https://github.com/jenkinsci/credentials-plugin.git'
        +
        +

        + + Example: Git step with git protocol and polling disabled +

        + Checkout from the Jenkins platform labeler repository using git protocol, no credentials, the master branch, and no polling for changes. + If poll is false, then the remote repository will not be polled for changes. + If poll is true or is not set, then the remote repository will be polled for changes. + See the workflow scm step documentation for more polling details. +

        The Pipeline Snippet Generator generates this example: +

        +git poll: false,
        +    url: 'git://github.com/jenkinsci/platformlabeler-plugin.git'
        +
        +

        + +
        diff --git a/src/main/resources/jenkins/plugins/git/GitStep/help_ja.html b/src/main/resources/jenkins/plugins/git/GitStep/help_ja.html new file mode 100644 index 0000000000..d6cac5d48d --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/GitStep/help_ja.html @@ -0,0 +1,11 @@ +
        +

        + Gitステップです。 指定したリポジトリからクローンを実行します。 +

        +

        + このステップは、一般的なSCMのステップである +checkout([$class: 'GitSCM', branches: [[name: '*/master']], + userRemoteConfigs: [[url: 'http://git-server/user/repository.git']]]) +   の短縮形です。  +

        +
        diff --git a/src/main/resources/jenkins/plugins/git/Messages.properties b/src/main/resources/jenkins/plugins/git/Messages.properties index b9a2d1b8b8..8eff912118 100644 --- a/src/main/resources/jenkins/plugins/git/Messages.properties +++ b/src/main/resources/jenkins/plugins/git/Messages.properties @@ -21,4 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -GitSCMSource.DisplayName=Git \ No newline at end of file +GitSCMSource.DisplayName=Git +GitStep.git=Git +within.Repository=Within Repository +additional=Additional diff --git a/src/main/resources/jenkins/plugins/git/Messages_ja.properties b/src/main/resources/jenkins/plugins/git/Messages_ja.properties new file mode 100644 index 0000000000..926ce3f6a1 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/Messages_ja.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2016-, Seiji Sogabe +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +GitSCMSource.DisplayName=Git +GitStep.git=Git diff --git a/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/config.jelly new file mode 100644 index 0000000000..92acdaa269 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/config.jelly @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/help.html b/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/help.html new file mode 100644 index 0000000000..d3ab0bfdb2 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/BranchDiscoveryTrait/help.html @@ -0,0 +1,3 @@ +
        + Discovers branches on the repository. +
        diff --git a/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/config.jelly new file mode 100644 index 0000000000..e616f9f8f9 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/config.jelly @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-nameMapping.html b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-nameMapping.html new file mode 100644 index 0000000000..3739844de7 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-nameMapping.html @@ -0,0 +1,9 @@ +

        + Mapping for how the ref can be named in for example the @Library.
        + Example: test-@{1}
        + Where @{1} replaces the first wildcard in the ref when discovered. +

        +

        + By default it will be "namespace_before_wildcard-@{1}". E.g. if ref is "test/*/merged" the default mapping would be + "test-@{1}". +

        \ No newline at end of file diff --git a/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-ref.html b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-ref.html new file mode 100644 index 0000000000..dc00a22d52 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help-ref.html @@ -0,0 +1,4 @@ +

        +The pattern under /refs on the remote repository to discover, can contain a wildcard.
        +Example: test/*/merged +

        \ No newline at end of file diff --git a/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help.html b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help.html new file mode 100644 index 0000000000..8b937f5092 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/DiscoverOtherRefsTrait/help.html @@ -0,0 +1,3 @@ +
        + Discovers other specified refs on the repository. +
        \ No newline at end of file diff --git a/src/main/resources/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait/config.jelly new file mode 100644 index 0000000000..2c1ca65834 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/GitBrowserSCMSourceTrait/config.jelly @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/GitSCMExtensionTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/GitSCMExtensionTrait/config.jelly new file mode 100644 index 0000000000..baebf407a2 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/GitSCMExtensionTrait/config.jelly @@ -0,0 +1,29 @@ + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/GitToolSCMSourceTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/GitToolSCMSourceTrait/config.jelly new file mode 100644 index 0000000000..300b7c06fd --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/GitToolSCMSourceTrait/config.jelly @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/LocalBranchTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/LocalBranchTrait/config.jelly new file mode 100644 index 0000000000..7497cb9fe9 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/LocalBranchTrait/config.jelly @@ -0,0 +1,27 @@ + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/Messages.properties b/src/main/resources/jenkins/plugins/git/traits/Messages.properties new file mode 100644 index 0000000000..0362cfd95b --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/Messages.properties @@ -0,0 +1,7 @@ +BranchDiscoveryTrait.authorityDisplayName=Trust branches +BranchDiscoveryTrait.displayName=Discover branches +TagDiscoveryTrait.authorityDisplayName=Trust tags +TagDiscoveryTrait.displayName=Discover tags +DiscoverOtherRefsTrait.displayName=Discover other refs + +Advanced.clone.behaviours=Advanced clone behaviours diff --git a/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/config.jelly b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/config.jelly new file mode 100644 index 0000000000..7864a6e50e --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/config.jelly @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/help-value.html b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/help-value.html new file mode 100644 index 0000000000..d714620ad3 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/RefSpecTemplate/help-value.html @@ -0,0 +1,4 @@ +
        + A ref spec to fetch. Any occurrences of @{remote} will be replaced by the remote name + (which defaults to origin) before use. +
        diff --git a/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/config.jelly new file mode 100644 index 0000000000..fb850a4943 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/RefSpecsSCMSourceTrait/config.jelly @@ -0,0 +1,37 @@ + + + + + + + + +
        + +
        +
        +
        +
        +
        diff --git a/src/main/resources/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait/config.jelly new file mode 100644 index 0000000000..0709d7869c --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/RemoteNameSCMSourceTrait/config.jelly @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly new file mode 100644 index 0000000000..92acdaa269 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help.html b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help.html new file mode 100644 index 0000000000..ea27046d41 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help.html @@ -0,0 +1,3 @@ +
        + Discovers tags on the repository. +
        diff --git a/src/main/webapp/extraRepo.html b/src/main/webapp/extraRepo.html index 71ae2058f2..e12f8ecc5c 100644 --- a/src/main/webapp/extraRepo.html +++ b/src/main/webapp/extraRepo.html @@ -1,4 +1,4 @@
        -Specify extra repositories that will be fetched. You can use this to add references to all the git repos of all +Specify extra repositories that will be fetched. You can use this to add references to all the git repos of all your team members, and the Jenkins build will fetch from them as well as from the 'central' repo.
        diff --git a/src/main/webapp/gitPublisher_ja.html b/src/main/webapp/gitPublisher_ja.html new file mode 100644 index 0000000000..e358db6c15 --- /dev/null +++ b/src/main/webapp/gitPublisher_ja.html @@ -0,0 +1,3 @@ +
        +オプションで、リモートリポジトリに、マージした結果やタグ、およびブランチをプッシュします。 +
        diff --git a/src/test/java/hudson/plugins/git/AbstractGitProject.java b/src/test/java/hudson/plugins/git/AbstractGitProject.java new file mode 100644 index 0000000000..f57c35b0b6 --- /dev/null +++ b/src/test/java/hudson/plugins/git/AbstractGitProject.java @@ -0,0 +1,267 @@ +/* + * The MIT License + * + * Copyright 2015 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.EnvVars; +import hudson.matrix.MatrixBuild; +import hudson.matrix.MatrixProject; +import hudson.model.AbstractBuild; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Node; +import hudson.model.Result; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.DisableRemotePoll; +import hudson.plugins.git.extensions.impl.EnforceGitClient; +import hudson.plugins.git.extensions.impl.PathRestriction; +import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; +import hudson.plugins.git.extensions.impl.SparseCheckoutPath; +import hudson.plugins.git.extensions.impl.SparseCheckoutPaths; +import hudson.plugins.git.extensions.impl.UserExclusion; +import hudson.remoting.VirtualChannel; +import hudson.slaves.EnvironmentVariablesNodeProperty; +import hudson.triggers.SCMTrigger; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import jenkins.MasterToSlaveFileCallable; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.JGitTool; + +import static org.junit.Assert.*; + +import org.junit.Rule; + +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Abstract class that provides convenience methods to configure projects. + * @author Mark Waite + */ +public class AbstractGitProject extends AbstractGitRepository { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + protected FreeStyleProject setupProject(List branches, boolean authorOrCommitter) throws Exception { + FreeStyleProject project = jenkins.createFreeStyleProject(); + GitSCM scm = new GitSCM(remoteConfigs(), branches, false, + Collections.emptyList(), null, null, + Collections.singletonList(new DisableRemotePoll())); + project.setScm(scm); + project.getBuildersList().add(new CaptureEnvironmentBuilder()); + return project; + } + + protected FreeStyleProject setupSimpleProject(String branchString) throws Exception { + return setupProject(Collections.singletonList(new BranchSpec(branchString)), false); + } + + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter) throws Exception { + return setupProject(branchString, authorOrCommitter, null); + } + + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter, + String relativeTargetDir) throws Exception { + return setupProject(branchString, authorOrCommitter, relativeTargetDir, null, null, null); + } + + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter, + String relativeTargetDir, + String excludedRegions, + String excludedUsers, + String includedRegions) throws Exception { + return setupProject(branchString, authorOrCommitter, relativeTargetDir, excludedRegions, excludedUsers, null, false, includedRegions); + } + + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter, + String relativeTargetDir, + String excludedRegions, + String excludedUsers, + boolean fastRemotePoll, + String includedRegions) throws Exception { + return setupProject(branchString, authorOrCommitter, relativeTargetDir, excludedRegions, excludedUsers, null, fastRemotePoll, includedRegions); + } + + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter, + String relativeTargetDir, String excludedRegions, + String excludedUsers, String localBranch, boolean fastRemotePoll, + String includedRegions) throws Exception { + return setupProject(Collections.singletonList(new BranchSpec(branchString)), + authorOrCommitter, relativeTargetDir, excludedRegions, + excludedUsers, localBranch, fastRemotePoll, + includedRegions); + } + + protected FreeStyleProject setupProject(List branches, boolean authorOrCommitter, + String relativeTargetDir, String excludedRegions, + String excludedUsers, String localBranch, boolean fastRemotePoll, + String includedRegions) throws Exception { + return setupProject(branches, + authorOrCommitter, relativeTargetDir, excludedRegions, + excludedUsers, localBranch, fastRemotePoll, + includedRegions, null); + } + + protected FreeStyleProject setupProject(String branchString, List sparseCheckoutPaths) throws Exception { + return setupProject(Collections.singletonList(new BranchSpec(branchString)), + false, null, null, + null, null, false, + null, sparseCheckoutPaths); + } + + protected FreeStyleProject setupProject(List branches, boolean authorOrCommitter, + String relativeTargetDir, String excludedRegions, + String excludedUsers, String localBranch, boolean fastRemotePoll, + String includedRegions, List sparseCheckoutPaths) throws Exception { + FreeStyleProject project = jenkins.createFreeStyleProject(); + GitSCM scm = new GitSCM( + remoteConfigs(), + branches, + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new DisableRemotePoll()); // don't work on a file:// repository + if (relativeTargetDir != null) { + scm.getExtensions().add(new RelativeTargetDirectory(relativeTargetDir)); + } + if (excludedUsers != null) { + scm.getExtensions().add(new UserExclusion(excludedUsers)); + } + if (excludedRegions != null || includedRegions != null) { + scm.getExtensions().add(new PathRestriction(includedRegions, excludedRegions)); + } + + scm.getExtensions().add(new SparseCheckoutPaths(sparseCheckoutPaths)); + + project.setScm(scm); + project.getBuildersList().add(new CaptureEnvironmentBuilder()); + return project; + } + + /** + * Creates a new project and configures the GitSCM according the parameters. + * + * @param repos git remote repositories + * @param branchSpecs branch specs + * @param scmTriggerSpec scm trigger spec + * @param disableRemotePoll disable workspace-less polling via "git ls-remote" + * @param enforceGitClient enforce git client + * @return the created project + * @throws Exception on error + */ + protected FreeStyleProject setupProject(List repos, List branchSpecs, + String scmTriggerSpec, boolean disableRemotePoll, EnforceGitClient enforceGitClient) throws Exception { + FreeStyleProject project = jenkins.createFreeStyleProject(); + GitSCM scm = new GitSCM( + repos, + branchSpecs, + false, Collections.emptyList(), + null, JGitTool.MAGIC_EXENAME, + Collections.emptyList()); + if (disableRemotePoll) { + scm.getExtensions().add(new DisableRemotePoll()); + } + if (enforceGitClient != null) { + scm.getExtensions().add(enforceGitClient); + } + project.setScm(scm); + if (scmTriggerSpec != null) { + SCMTrigger trigger = new SCMTrigger(scmTriggerSpec); + project.addTrigger(trigger); + trigger.start(project, true); + } + project.getBuildersList().add(new CaptureEnvironmentBuilder()); + project.save(); + return project; + } + + protected FreeStyleBuild build(final FreeStyleProject project, final Result expectedResult, final String... expectedNewlyCommittedFiles) throws Exception { + final FreeStyleBuild build = project.scheduleBuild2(0).get(); + for (final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { + assertTrue(expectedNewlyCommittedFile + " file not found in workspace", build.getWorkspace().child(expectedNewlyCommittedFile).exists()); + } + if (expectedResult != null) { + jenkins.assertBuildStatus(expectedResult, build); + } + return build; + } + + protected FreeStyleBuild build(final FreeStyleProject project, final String parentDir, final Result expectedResult, final String... expectedNewlyCommittedFiles) throws Exception { + final FreeStyleBuild build = project.scheduleBuild2(0).get(); + for (final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { + assertTrue(build.getWorkspace().child(parentDir).child(expectedNewlyCommittedFile).exists()); + } + if (expectedResult != null) { + jenkins.assertBuildStatus(expectedResult, build); + } + return build; + } + + protected MatrixBuild build(final MatrixProject project, final Result expectedResult, final String... expectedNewlyCommittedFiles) throws Exception { + final MatrixBuild build = project.scheduleBuild2(0).get(); + for (final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { + assertTrue(expectedNewlyCommittedFile + " file not found in workspace", build.getWorkspace().child(expectedNewlyCommittedFile).exists()); + } + if (expectedResult != null) { + jenkins.assertBuildStatus(expectedResult, build); + } + return build; + } + + protected String getHeadRevision(AbstractBuild build, final String branch) throws IOException, InterruptedException { + return build.getWorkspace().act(new MasterToSlaveFileCallable() { + public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + try (Repository repo = Git.with(null, null).in(f).getClient().getRepository()) { + return repo.resolve("refs/heads/" + branch).name(); + } catch (GitException e) { + throw new RuntimeException(e); + } + } + }); + } + + protected EnvVars getEnvVars(FreeStyleProject project) { + for (hudson.tasks.Builder b : project.getBuilders()) { + if (b instanceof CaptureEnvironmentBuilder) { + return ((CaptureEnvironmentBuilder) b).getEnvVars(); + } + } + return new EnvVars(); + } + + protected void setVariables(Node node, EnvironmentVariablesNodeProperty.Entry... entries) throws IOException { + node.getNodeProperties().replaceBy( + Collections.singleton(new EnvironmentVariablesNodeProperty( + entries))); + + } +} diff --git a/src/test/java/hudson/plugins/git/AbstractGitRepository.java b/src/test/java/hudson/plugins/git/AbstractGitRepository.java new file mode 100644 index 0000000000..ef059d4899 --- /dev/null +++ b/src/test/java/hudson/plugins/git/AbstractGitRepository.java @@ -0,0 +1,84 @@ +package hudson.plugins.git; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; + +import jenkins.plugins.git.GitSampleRepoRule; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +/** + * Temporary git repository for use with tests. Tests which need a git + * repository but do not need a running Jenkins instance should extend this + * class so that repository setup and teardown are handled for them. + * + * Provides convenience methods for various repository functions. + * + * @author Mark Waite + */ +public abstract class AbstractGitRepository { + + protected File testGitDir; + protected GitClient testGitClient; + + @Rule + public GitSampleRepoRule repo = new GitSampleRepoRule(); + + @Before + public void createGitRepository() throws Exception { + TaskListener listener = StreamTaskListener.fromStderr(); + repo.init(); + testGitDir = repo.getRoot(); + testGitClient = Git.with(listener, new EnvVars()).in(testGitDir).getClient(); + } + + /** + * Commit fileName to this git repository + * + * @param fileName name of file to create + * @throws GitException on git error + * @throws InterruptedException when interrupted + */ + protected void commitNewFile(final String fileName) throws GitException, InterruptedException { + File newFile = new File(testGitDir, fileName); + assert !newFile.exists(); // Not expected to use commitNewFile to update existing file + try (PrintWriter writer = new PrintWriter(newFile, "UTF-8")) { + writer.println("A file named " + fileName); + writer.close(); + testGitClient.add(fileName); + testGitClient.commit("Added a file named " + fileName); + } catch (FileNotFoundException | UnsupportedEncodingException notFound) { + throw new GitException(notFound); + } + } + + /** + * Returns list of UserRemoteConfig for this repository. + * + * @return list of UserRemoteConfig for this repository + * @throws IOException on input or output error + */ + protected List remoteConfigs() throws IOException { + List list = new ArrayList<>(); + list.add(new UserRemoteConfig(testGitDir.getAbsolutePath(), "origin", "", null)); + return list; + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return File.pathSeparatorChar==';'; + } +} diff --git a/src/test/java/hudson/plugins/git/AbstractGitTestCase.java b/src/test/java/hudson/plugins/git/AbstractGitTestCase.java index 8c57b1817d..50c5d96141 100644 --- a/src/test/java/hudson/plugins/git/AbstractGitTestCase.java +++ b/src/test/java/hudson/plugins/git/AbstractGitTestCase.java @@ -1,11 +1,25 @@ package hudson.plugins.git; -import com.google.common.collect.Lists; +import com.cloudbees.plugins.credentials.CredentialsNameProvider; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; + import hudson.EnvVars; import hudson.FilePath; -import hudson.model.*; +import hudson.Launcher; +import hudson.matrix.MatrixBuild; +import hudson.matrix.MatrixProject; +import hudson.model.FreeStyleBuild; +import hudson.model.Result; +import hudson.model.TaskListener; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.FreeStyleProject; +import hudson.model.Node; import hudson.plugins.git.extensions.GitSCMExtension; -import hudson.plugins.git.extensions.impl.CleanBeforeCheckout; +import hudson.plugins.git.extensions.impl.EnforceGitClient; import hudson.plugins.git.extensions.impl.DisableRemotePoll; import hudson.plugins.git.extensions.impl.PathRestriction; import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; @@ -14,20 +28,32 @@ import hudson.plugins.git.extensions.impl.UserExclusion; import hudson.remoting.VirtualChannel; import hudson.slaves.EnvironmentVariablesNodeProperty; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.triggers.SCMTrigger; import hudson.util.StreamTaskListener; import java.io.File; import java.io.IOException; +import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; +import jenkins.MasterToSlaveFileCallable; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.JGitTool; +import org.junit.Before; +import org.junit.Rule; +import jenkins.plugins.git.GitSampleRepoRule; import org.jvnet.hudson.test.CaptureEnvironmentBuilder; -import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import static org.junit.Assert.*; +import org.kohsuke.stapler.DataBoundConstructor; /** * Base class for single repository git plugin tests. @@ -35,25 +61,29 @@ * @author Kohsuke Kawaguchi * @author ishaaq */ -public abstract class AbstractGitTestCase extends HudsonTestCase { - protected TaskListener listener; - - protected TestGitRepo testRepo; - - // aliases of testRepo properties - protected PersonIdent johnDoe; - protected PersonIdent janeDoe; - protected File workDir; // aliases "gitDir" - protected FilePath workspace; // aliases "gitDirPath" - protected GitClient git; - - @Override - protected void setUp() throws Exception { - super.setUp(); +public abstract class AbstractGitTestCase { + @Rule + public JenkinsRule rule = new JenkinsRule(); + + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + protected TaskListener listener; + protected TestGitRepo testRepo; + + // aliases of testRepo properties + protected PersonIdent johnDoe; + protected PersonIdent janeDoe; + protected File workDir; // aliases "gitDir" + protected FilePath workspace; // aliases "gitDirPath" + protected GitClient git; + + @Before + public void setUp() throws Exception { listener = StreamTaskListener.fromStderr(); - testRepo = new TestGitRepo("unnamed", this, listener); + testRepo = new TestGitRepo("unnamed", sampleRepo.getRoot(), listener); johnDoe = testRepo.johnDoe; janeDoe = testRepo.janeDoe; workDir = testRepo.gitDir; @@ -61,26 +91,34 @@ protected void setUp() throws Exception { git = testRepo.git; } - protected void commit(final String fileName, final PersonIdent committer, final String message) + protected String commit(final String fileName, final PersonIdent committer, final String message) throws GitException, InterruptedException { - testRepo.commit(fileName, committer, message); + return testRepo.commit(fileName, committer, message); } - protected void commit(final String fileName, final String fileContent, final PersonIdent committer, final String message) + protected String commit(final String fileName, final String fileContent, final PersonIdent committer, final String message) throws GitException, InterruptedException { - testRepo.commit(fileName, fileContent, committer, message); + return testRepo.commit(fileName, fileContent, committer, message); } - protected void commit(final String fileName, final PersonIdent author, final PersonIdent committer, + protected String commit(final String fileName, final PersonIdent author, final PersonIdent committer, final String message) throws GitException, InterruptedException { - testRepo.commit(fileName, author, committer, message); + return testRepo.commit(fileName, author, committer, message); } protected List createRemoteRepositories() throws IOException { return testRepo.remoteConfigs(); } + protected List createRemoteRepositories(StandardCredentials credential) throws IOException { + return testRepo.remoteConfigs(credential); + } + + protected FreeStyleProject createFreeStyleProject() throws IOException { + return rule.createFreeStyleProject(); + } + protected FreeStyleProject setupProject(String branchString, boolean authorOrCommitter) throws Exception { return setupProject(branchString, authorOrCommitter, null); } @@ -117,6 +155,13 @@ protected FreeStyleProject setupProject(String branchString, boolean authorOrCom includedRegions); } + protected FreeStyleProject setupProject(String branchString, StandardCredentials credential) throws Exception { + return setupProject(Collections.singletonList(new BranchSpec(branchString)), + false, null, null, + null, null, false, + null, null, credential); + } + protected FreeStyleProject setupProject(List branches, boolean authorOrCommitter, String relativeTargetDir, String excludedRegions, String excludedUsers, String localBranch, boolean fastRemotePoll, @@ -124,27 +169,31 @@ protected FreeStyleProject setupProject(List branches, boolean autho return setupProject(branches, authorOrCommitter, relativeTargetDir, excludedRegions, excludedUsers, localBranch, fastRemotePoll, - includedRegions, null); + includedRegions, null, null); } protected FreeStyleProject setupProject(String branchString, List sparseCheckoutPaths) throws Exception { return setupProject(Collections.singletonList(new BranchSpec(branchString)), false, null, null, null, null, false, - null, sparseCheckoutPaths); + null, sparseCheckoutPaths, null); } protected FreeStyleProject setupProject(List branches, boolean authorOrCommitter, - String relativeTargetDir, String excludedRegions, - String excludedUsers, String localBranch, boolean fastRemotePoll, - String includedRegions, List sparseCheckoutPaths) throws Exception { + String relativeTargetDir, String excludedRegions, + String excludedUsers, String localBranch, boolean fastRemotePoll, + String includedRegions, List sparseCheckoutPaths, + StandardCredentials credential) throws Exception { FreeStyleProject project = createFreeStyleProject(); GitSCM scm = new GitSCM( - createRemoteRepositories(), + createRemoteRepositories(credential), branches, false, Collections.emptyList(), null, null, Collections.emptyList()); + if (credential != null) { + project.getBuildersList().add(new HasCredentialBuilder(credential.getId())); + } scm.getExtensions().add(new DisableRemotePoll()); // don't work on a file:// repository if (relativeTargetDir!=null) scm.getExtensions().add(new RelativeTargetDirectory(relativeTargetDir)); @@ -160,33 +209,75 @@ protected FreeStyleProject setupProject(List branches, boolean autho return project; } + /** + * Creates a new project and configures the GitSCM according the parameters. + * @param repos git remote repositories + * @param branchSpecs branch specs + * @param scmTriggerSpec scm trigger spec + * @param disableRemotePoll disable workspace-less polling via "git ls-remote" + * @param enforceGitClient enforce git client + * @return the created project + * @throws Exception on error + */ + protected FreeStyleProject setupProject(List repos, List branchSpecs, + String scmTriggerSpec, boolean disableRemotePoll, EnforceGitClient enforceGitClient) throws Exception { + FreeStyleProject project = createFreeStyleProject(); + GitSCM scm = new GitSCM( + repos, + branchSpecs, + false, Collections.emptyList(), + null, JGitTool.MAGIC_EXENAME, + Collections.emptyList()); + if(disableRemotePoll) scm.getExtensions().add(new DisableRemotePoll()); + if(enforceGitClient != null) scm.getExtensions().add(enforceGitClient); + project.setScm(scm); + if(scmTriggerSpec != null) { + SCMTrigger trigger = new SCMTrigger(scmTriggerSpec); + project.addTrigger(trigger); + trigger.start(project, true); + } + //project.getBuildersList().add(new CaptureEnvironmentBuilder()); + project.save(); + return project; + } + protected FreeStyleProject setupSimpleProject(String branchString) throws Exception { return setupProject(branchString,false); } protected FreeStyleBuild build(final FreeStyleProject project, final Result expectedResult, final String...expectedNewlyCommittedFiles) throws Exception { - final FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserCause()).get(); - System.out.println(build.getLog()); + final FreeStyleBuild build = project.scheduleBuild2(0).get(); for(final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { assertTrue(expectedNewlyCommittedFile + " file not found in workspace", build.getWorkspace().child(expectedNewlyCommittedFile).exists()); } if(expectedResult != null) { - assertBuildStatus(expectedResult, build); + rule.assertBuildStatus(expectedResult, build); } return build; } protected FreeStyleBuild build(final FreeStyleProject project, final String parentDir, final Result expectedResult, final String...expectedNewlyCommittedFiles) throws Exception { - final FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserCause()).get(); - System.out.println(build.getLog()); + final FreeStyleBuild build = project.scheduleBuild2(0).get(); for(final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { assertTrue(build.getWorkspace().child(parentDir).child(expectedNewlyCommittedFile).exists()); } if(expectedResult != null) { - assertBuildStatus(expectedResult, build); + rule.assertBuildStatus(expectedResult, build); } return build; } + + protected MatrixBuild build(final MatrixProject project, final Result expectedResult, final String...expectedNewlyCommittedFiles) throws Exception { + final MatrixBuild build = project.scheduleBuild2(0).get(); + for(final String expectedNewlyCommittedFile : expectedNewlyCommittedFiles) { + assertTrue(expectedNewlyCommittedFile + " file not found in workspace", build.getWorkspace().child(expectedNewlyCommittedFile).exists()); + } + if(expectedResult != null) { + rule.assertBuildStatus(expectedResult, build); + } + return build; + } + protected EnvVars getEnvVars(FreeStyleProject project) { for (hudson.tasks.Builder b : project.getBuilders()) { @@ -205,7 +296,7 @@ protected void setVariables(Node node, EnvironmentVariablesNodeProperty.Entry... } protected String getHeadRevision(AbstractBuild build, final String branch) throws IOException, InterruptedException { - return build.getWorkspace().act(new FilePath.FileCallable() { + return build.getWorkspace().act(new MasterToSlaveFileCallable() { public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { try { ObjectId oid = Git.with(null, null).in(f).getClient().getRepository().resolve("refs/heads/" + branch); @@ -214,6 +305,49 @@ public String invoke(File f, VirtualChannel channel) throws IOException, Interru throw new RuntimeException(e); } } + }); } + + public static class HasCredentialBuilder extends Builder { + + private final String id; + + @DataBoundConstructor + public HasCredentialBuilder(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) + throws InterruptedException, IOException { + IdCredentials credentials = CredentialsProvider.findCredentialById(id, IdCredentials.class, build); + if (credentials == null) { + listener.getLogger().printf("Could not find any credentials with id %s%n", id); + build.setResult(Result.FAILURE); + return false; + } else { + listener.getLogger().printf("Found %s credentials with id %s%n", CredentialsNameProvider.name(credentials), id); + return true; + } + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + @Override + public String getDisplayName() { + return "Check that credentials exist"; + } + } + } } diff --git a/src/test/java/hudson/plugins/git/BranchSpecTest.java b/src/test/java/hudson/plugins/git/BranchSpecTest.java new file mode 100644 index 0000000000..58d42e83e4 --- /dev/null +++ b/src/test/java/hudson/plugins/git/BranchSpecTest.java @@ -0,0 +1,237 @@ +package hudson.plugins.git; + +import hudson.EnvVars; +import java.util.HashMap; +import static org.junit.Assert.*; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; + + +public class BranchSpecTest { + @Test + public void testMatch() { + + BranchSpec l = new BranchSpec("master"); + assertTrue(l.matches("origin/master")); + assertFalse(l.matches("origin/something/master")); + assertTrue(l.matches("master")); + assertFalse(l.matches("dev")); + + + BranchSpec est = new BranchSpec("origin/*/dev"); + + assertFalse(est.matches("origintestdev")); + assertTrue(est.matches("origin/test/dev")); + assertFalse(est.matches("origin/test/release")); + assertFalse(est.matches("origin/test/something/release")); + + BranchSpec s = new BranchSpec("origin/*"); + + assertTrue(s.matches("origin/master")); + + BranchSpec m = new BranchSpec("**/magnayn/*"); + + assertTrue(m.matches("origin/magnayn/b1")); + assertTrue(m.matches("remote/origin/magnayn/b1")); + assertTrue(m.matches("remotes/origin/magnayn/b1")); + + BranchSpec n = new BranchSpec("*/my.branch/*"); + + assertTrue(n.matches("origin/my.branch/b1")); + assertFalse(n.matches("origin/my-branch/b1")); + assertFalse(n.matches("remote/origin/my.branch/b1")); + assertTrue(n.matches("remotes/origin/my.branch/b1")); + + BranchSpec o = new BranchSpec("**"); + + assertTrue(o.matches("origin/my.branch/b1")); + assertTrue(o.matches("origin/my-branch/b1")); + assertTrue(o.matches("remote/origin/my.branch/b1")); + assertTrue(o.matches("remotes/origin/my.branch/b1")); + + BranchSpec p = new BranchSpec("*"); + + assertTrue(p.matches("origin/x")); + assertFalse(p.matches("origin/my-branch/b1")); + } + + @Test + public void testMatchEnv() { + HashMap envMap = new HashMap<>(); + envMap.put("master", "master"); + envMap.put("origin", "origin"); + envMap.put("dev", "dev"); + envMap.put("magnayn", "magnayn"); + envMap.put("mybranch", "my.branch"); + envMap.put("anyLong", "**"); + envMap.put("anyShort", "*"); + envMap.put("anyEmpty", ""); + EnvVars env = new EnvVars(envMap); + + BranchSpec l = new BranchSpec("${master}"); + assertTrue(l.matches("origin/master", env)); + assertFalse(l.matches("origin/something/master", env)); + assertTrue(l.matches("master", env)); + assertFalse(l.matches("dev", env)); + + + BranchSpec est = new BranchSpec("${origin}/*/${dev}"); + + assertFalse(est.matches("origintestdev", env)); + assertTrue(est.matches("origin/test/dev", env)); + assertFalse(est.matches("origin/test/release", env)); + assertFalse(est.matches("origin/test/something/release", env)); + + BranchSpec s = new BranchSpec("${origin}/*"); + + assertTrue(s.matches("origin/master", env)); + + BranchSpec m = new BranchSpec("**/${magnayn}/*"); + + assertTrue(m.matches("origin/magnayn/b1", env)); + assertTrue(m.matches("remote/origin/magnayn/b1", env)); + + BranchSpec n = new BranchSpec("*/${mybranch}/*"); + + assertTrue(n.matches("origin/my.branch/b1", env)); + assertFalse(n.matches("origin/my-branch/b1", env)); + assertFalse(n.matches("remote/origin/my.branch/b1", env)); + + BranchSpec o = new BranchSpec("${anyLong}"); + + assertTrue(o.matches("origin/my.branch/b1", env)); + assertTrue(o.matches("origin/my-branch/b1", env)); + assertTrue(o.matches("remote/origin/my.branch/b1", env)); + + BranchSpec p = new BranchSpec("${anyShort}"); + + assertTrue(p.matches("origin/x", env)); + assertFalse(p.matches("origin/my-branch/b1", env)); + + BranchSpec q = new BranchSpec("${anyEmpty}"); + + assertTrue(q.matches("origin/my.branch/b1", env)); + assertTrue(q.matches("origin/my-branch/b1", env)); + assertTrue(q.matches("remote/origin/my.branch/b1", env)); + } + + @Test + public void testEmptyName() { + BranchSpec branchSpec = new BranchSpec(""); + assertEquals("**",branchSpec.getName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullName() { + BranchSpec branchSpec = new BranchSpec(null); + } + + @Test + public void testNameTrimming() { + BranchSpec branchSpec = new BranchSpec(" master "); + assertEquals("master",branchSpec.getName()); + branchSpec.setName(" other "); + assertEquals("other",branchSpec.getName()); + } + + @Test + public void testUsesRefsHeads() { + BranchSpec m = new BranchSpec("refs/heads/j*n*"); + assertTrue(m.matches("refs/heads/jenkins")); + assertTrue(m.matches("refs/heads/jane")); + assertTrue(m.matches("refs/heads/jones")); + + assertFalse(m.matches("origin/jenkins")); + assertFalse(m.matches("remote/origin/jane")); + } + + @Test + public void testUsesJavaPatternDirectlyIfPrefixedWithColon() { + BranchSpec m = new BranchSpec(":^(?!(origin/prefix)).*"); + assertTrue(m.matches("origin")); + assertTrue(m.matches("origin/master")); + assertTrue(m.matches("origin/feature")); + + assertFalse(m.matches("origin/prefix_123")); + assertFalse(m.matches("origin/prefix")); + assertFalse(m.matches("origin/prefix-abc")); + } + + @Test + @Issue("JENKINS-26842") + public void testUsesJavaPatternWithRepetition() { + // match pattern from JENKINS-26842 + BranchSpec m = new BranchSpec(":origin/release-\\d{8}"); + assertTrue(m.matches("origin/release-20150101")); + assertFalse(m.matches("origin/release-2015010")); + assertFalse(m.matches("origin/release-201501011")); + assertFalse(m.matches("origin/release-20150101-something")); + } + + @Test + public void testUsesJavaPatternToExcludeMultipleBranches() { + BranchSpec m = new BranchSpec(":^(?!origin/master$|origin/develop$).*"); + assertTrue(m.matches("origin/branch1")); + assertTrue(m.matches("origin/branch-2")); + assertTrue(m.matches("origin/master123")); + assertTrue(m.matches("origin/develop-123")); + assertFalse(m.matches("origin/master")); + assertFalse(m.matches("origin/develop")); + } + + private EnvVars createEnvMap(String key, String value) { + HashMap envMap = new HashMap<>(); + envMap.put(key, value); + return new EnvVars(envMap); + } + + /* BranchSpec does not seem to honor token macros. For example, + * $GIT_BRANCH matches as expected + * ${GIT_BRANCH} matches as expected + * ${GIT_BRANCH,fullName=False} does not match + * ${GIT_BRANCH,fullName=True} does not match + */ + @Test + @Issue("JENKINS-6856") + public void testUsesEnvValueWithBraces() { + EnvVars env = createEnvMap("GIT_BRANCH", "origin/master"); + + BranchSpec withBraces = new BranchSpec("${GIT_BRANCH}"); + assertTrue(withBraces.matches("refs/heads/origin/master", env)); + assertTrue(withBraces.matches("origin/master", env)); + assertFalse(withBraces.matches("master", env)); + } + + @Test + @Issue("JENKINS-6856") + public void testUsesEnvValueWithoutBraces() { + EnvVars env = createEnvMap("GIT_BRANCH", "origin/master"); + + BranchSpec withoutBraces = new BranchSpec("$GIT_BRANCH"); + assertTrue(withoutBraces.matches("refs/heads/origin/master", env)); + assertTrue(withoutBraces.matches("origin/master", env)); + assertFalse(withoutBraces.matches("master", env)); + } + + @Test + @Issue("JENKINS-6856") + public void testUsesEnvValueWithToken() { + EnvVars env = createEnvMap("GIT_BRANCH", "origin/master"); + + BranchSpec withToken = new BranchSpec("${GIT_BRANCH,fullName=True}"); + assertFalse(withToken.matches("refs/heads/origin/master", env)); + assertFalse(withToken.matches("origin/master", env)); + assertFalse(withToken.matches("master", env)); + } + + @Test + @Issue("JENKINS-6856") + public void testUsesEnvValueWithTokenFalse() { + EnvVars env = createEnvMap("GIT_BRANCH", "origin/master"); + + BranchSpec withTokenFalse = new BranchSpec("${GIT_BRANCH,fullName=false}"); + assertFalse(withTokenFalse.matches("refs/heads/origin/master", env)); + assertFalse(withTokenFalse.matches("origin/master", env)); + assertFalse(withTokenFalse.matches("master", env)); + } +} diff --git a/src/test/java/hudson/plugins/git/ChangelogToBranchOptionsTest.java b/src/test/java/hudson/plugins/git/ChangelogToBranchOptionsTest.java new file mode 100644 index 0000000000..3b5bd15ed7 --- /dev/null +++ b/src/test/java/hudson/plugins/git/ChangelogToBranchOptionsTest.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import org.junit.Test; +import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.*; + +public class ChangelogToBranchOptionsTest { + + private final ChangelogToBranchOptions options; + private final String compareRemote; + private final String compareTarget; + + public ChangelogToBranchOptionsTest() { + compareRemote = "origin"; + compareTarget = "feature/new-thing"; + options = new ChangelogToBranchOptions(compareRemote, compareTarget); + } + + @Test + public void testGetCompareRemote() { + assertThat(options.getCompareRemote(), is(compareRemote)); + } + + @Test + public void testGetCompareTarget() { + assertThat(options.getCompareTarget(), is(compareTarget)); + } + + @Test + public void testGetRef() { + assertThat(options.getRef(), is(compareRemote + "/" + compareTarget)); + } + + @Test + public void testAlternateConstructor() { + ChangelogToBranchOptions newOptions = new ChangelogToBranchOptions(options); + assertThat(newOptions.getCompareRemote(), is(options.getCompareRemote())); + assertThat(newOptions.getCompareTarget(), is(options.getCompareTarget())); + assertThat(newOptions, is(not(options))); // Does not implement equals + } +} diff --git a/src/test/java/hudson/plugins/git/CliGitSCMTriggerLocalPollTest.java b/src/test/java/hudson/plugins/git/CliGitSCMTriggerLocalPollTest.java new file mode 100644 index 0000000000..3a4ad2c3fb --- /dev/null +++ b/src/test/java/hudson/plugins/git/CliGitSCMTriggerLocalPollTest.java @@ -0,0 +1,21 @@ +package hudson.plugins.git; + +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.impl.EnforceGitClient; + +public class CliGitSCMTriggerLocalPollTest extends SCMTriggerTest +{ + + @Override + protected EnforceGitClient getGitClient() + { + return new EnforceGitClient().set(GitClientType.GITCLI); + } + + @Override + protected boolean isDisableRemotePoll() + { + return true; + } + +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/git/CliGitSCMTriggerRemotePollTest.java b/src/test/java/hudson/plugins/git/CliGitSCMTriggerRemotePollTest.java new file mode 100644 index 0000000000..43b2db913f --- /dev/null +++ b/src/test/java/hudson/plugins/git/CliGitSCMTriggerRemotePollTest.java @@ -0,0 +1,26 @@ +package hudson.plugins.git; + +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.impl.EnforceGitClient; + +/** + * Remote polling and local polling behave differently due to bugs in productive + * code which probably cannot be fixed without serious compatibility problems. + * The isChangeExpected() method adjusts the tests to the difference between + * local and remote polling. + */ +public class CliGitSCMTriggerRemotePollTest extends SCMTriggerTest { + + @Override + protected EnforceGitClient getGitClient() + { + return new EnforceGitClient().set(GitClientType.GITCLI); + } + + @Override + protected boolean isDisableRemotePoll() + { + return false; + } + +} diff --git a/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java b/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java new file mode 100644 index 0000000000..8130796f33 --- /dev/null +++ b/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java @@ -0,0 +1,141 @@ +package hudson.plugins.git; + +import com.cloudbees.plugins.credentials.*; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import jenkins.model.Jenkins; +import jenkins.plugins.git.GitSampleRepoRule; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.util.Collections; + +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +public class CredentialsUserRemoteConfigTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + private CredentialsStore store = null; + + @Before + public void enableSystemCredentialsProvider() { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap( + Collections.singletonMap(Domain.global(), Collections.emptyList())); + for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) { + if (s.getProvider() instanceof SystemCredentialsProvider.ProviderImpl) { + store = s; + break; + } + } + assertThat("The system credentials provider is enabled", store, notNullValue()); + } + + @Issue("JENKINS-30515") + @Test + public void checkoutWithValidCredentials() throws Exception { + sampleRepo.init(); + store.addCredentials(Domain.global(), createCredential(CredentialsScope.GLOBAL, "github")); + store.save(); + + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', \n" + + " userRemoteConfigs: [[credentialsId: 'github', url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("using credential github", b); + } + + @Issue("JENKINS-30515") + @Test + public void checkoutWithDifferentCredentials() throws Exception { + sampleRepo.init(); + store.addCredentials(Domain.global(), createCredential(CredentialsScope.GLOBAL, "other")); + store.save(); + + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', \n" + + " userRemoteConfigs: [[credentialsId: 'github', url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Warning: CredentialId \"github\" could not be found", b); + } + + @Issue("JENKINS-30515") + @Test + public void checkoutWithInvalidCredentials() throws Exception { + sampleRepo.init(); + store.addCredentials(Domain.global(), createCredential(CredentialsScope.SYSTEM, "github")); + store.save(); + + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', \n" + + " userRemoteConfigs: [[credentialsId: 'github', url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Warning: CredentialId \"github\" could not be found", b); + } + + @Issue("JENKINS-30515") + @Test + public void checkoutWithNoCredentialsStoredButUsed() throws Exception { + sampleRepo.init(); + + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', \n" + + " userRemoteConfigs: [[credentialsId: 'github', url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Warning: CredentialId \"github\" could not be found", b); + } + + @Issue("JENKINS-30515") + @Test + public void checkoutWithNoCredentialsSpecified() throws Exception { + sampleRepo.init(); + + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', \n" + + " userRemoteConfigs: [[url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("No credentials specified", b); + } + + + private StandardCredentials createCredential(CredentialsScope scope, String id) { + return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, "username", "password"); + } +} diff --git a/src/test/java/hudson/plugins/git/GitBranchSpecifierColumnTest.java b/src/test/java/hudson/plugins/git/GitBranchSpecifierColumnTest.java new file mode 100644 index 0000000000..23b608e89b --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitBranchSpecifierColumnTest.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.model.Item; +import java.util.ArrayList; +import java.util.List; +import static org.hamcrest.Matchers.*; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * @author Mark Waite + */ +public class GitBranchSpecifierColumnTest { + + public GitBranchSpecifierColumnTest() { + } + + @Test + public void testGetBranchSpecifierNull() { + Item item = null; + GitBranchSpecifierColumn branchSpecifierColumn = new GitBranchSpecifierColumn(); + List result = branchSpecifierColumn.getBranchSpecifier(item); + assertThat(result, is(emptyCollectionOf(String.class))); + } + + @Test + public void testBreakOutString() { + List branches = new ArrayList<>(); + final String MASTER_BRANCH = "master"; + branches.add(MASTER_BRANCH); + String DEVELOP_BRANCH = "develop"; + branches.add(DEVELOP_BRANCH); + GitBranchSpecifierColumn branchSpecifier = new GitBranchSpecifierColumn(); + String result = branchSpecifier.breakOutString(branches); + assertEquals(MASTER_BRANCH + ", " + DEVELOP_BRANCH, result); + } + + @Test + public void testBreakOutStringEmpty() { + List branches = new ArrayList<>(); + GitBranchSpecifierColumn branchSpecifier = new GitBranchSpecifierColumn(); + String result = branchSpecifier.breakOutString(branches); + assertEquals("", result); + } + + @Test + public void testBreakOutStringNull() { + List branches = null; + GitBranchSpecifierColumn branchSpecifier = new GitBranchSpecifierColumn(); + String result = branchSpecifier.breakOutString(branches); + assertEquals(null, result); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeLogParserTest.java b/src/test/java/hudson/plugins/git/GitChangeLogParserTest.java index e66740494a..a67feccfa8 100644 --- a/src/test/java/hudson/plugins/git/GitChangeLogParserTest.java +++ b/src/test/java/hudson/plugins/git/GitChangeLogParserTest.java @@ -1,37 +1,66 @@ package hudson.plugins.git; +import hudson.EnvVars; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.gitclient.CliGitAPIImpl; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.JGitAPIImpl; + import java.io.File; import java.io.FileWriter; - -import org.jvnet.hudson.test.HudsonTestCase; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** * Unit tests of {@link GitChangeLogParser} */ -public class GitChangeLogParserTest extends HudsonTestCase { - - /** - * Test duplicate changes filtered from parsed change set list. - * - * @throws Exception - */ - public void testDuplicatesFiltered() throws Exception { - GitChangeLogParser parser = new GitChangeLogParser(true); - File log = File.createTempFile(getClass().getName(), ".tmp"); - FileWriter writer = new FileWriter(log); - writer.write("commit 123abc456def\n"); - writer.write(" first message\n"); - writer.write("commit 123abc456def\n"); - writer.write(" second message"); - writer.close(); - GitChangeSetList list = parser.parse(null, log); +public class GitChangeLogParserTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + private final String firstMessageTruncated = "123456789 123456789 123456789 123456789 123456789 123456789 123456789 1"; + private final String firstMessage = firstMessageTruncated + " 345 789"; + + /* Test duplicate changes filtered from parsed CLI git change set list. */ + @Test + public void testDuplicatesFilteredCliGit() throws Exception { + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).using("Default").in(new File(".")).getClient(); + assertThat(gitClient, instanceOf(CliGitAPIImpl.class)); + /* JENKINS-29977 notes that CLI git impl truncates summary message - confirm default behavior retained */ + generateDuplicateChanges(gitClient, firstMessageTruncated); + } + + /* Test duplicate changes filtered from parsed JGit change set list. */ + @Test + public void testDuplicatesFilteredJGit() throws Exception { + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).using("jgit").in(new File(".")).getClient(); + assertThat(gitClient, instanceOf(JGitAPIImpl.class)); + /* JENKINS-29977 notes that JGit impl retains full summary message - confirm default behavior retained */ + generateDuplicateChanges(gitClient, firstMessage); + } + + private void generateDuplicateChanges(GitClient gitClient, String expectedMessage) throws Exception { + GitChangeLogParser parser = new GitChangeLogParser(gitClient, true); + File log = tmpFolder.newFile(); + try (FileWriter writer = new FileWriter(log)) { + writer.write("commit 123abc456def\n"); + writer.write(" " + firstMessage + "\n"); + writer.write("commit 123abc456def\n"); + writer.write(" second message"); + } + GitChangeSetList list = parser.parse(null, null, log); assertNotNull(list); assertNotNull(list.getLogs()); assertEquals(1, list.getLogs().size()); GitChangeSet first = list.getLogs().get(0); assertNotNull(first); assertEquals("123abc456def", first.getId()); - assertEquals("first message", first.getMsg()); + assertThat(first.getMsg(), is(expectedMessage)); assertTrue("Temp file delete failed for " + log, log.delete()); } } diff --git a/src/test/java/hudson/plugins/git/GitChangeSetBadArgsTest.java b/src/test/java/hudson/plugins/git/GitChangeSetBadArgsTest.java new file mode 100644 index 0000000000..001659e7e8 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetBadArgsTest.java @@ -0,0 +1,124 @@ +package hudson.plugins.git; + +import java.util.ArrayList; + +import hudson.model.User; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; + +import org.jvnet.hudson.test.JenkinsRule; + +public class GitChangeSetBadArgsTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + private GitChangeSet createChangeSet(boolean authorOrCommitter, String name, String email) { + String dataSource = authorOrCommitter ? "Author" : "Committer"; + ArrayList lines = new ArrayList<>(); + lines.add("commit 1567861636cd854f4dd6fa40bf94c0c657681dd5"); + lines.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); + lines.add("parent e74a24e995305bd67a180f0ebc57927e2b8783ce"); + if (authorOrCommitter) { + lines.add("author " + name + " <" + email + "> 1363879004 +0100"); + lines.add("committer Good Committer 1364199539 -0400"); + } else { + lines.add("author Good Author 1363879004 +0100"); + lines.add("committer " + name + " <" + email + "> 1364199539 -0400"); + } + lines.add(""); + lines.add(" " + dataSource + " has e-mail address '" + email + "' and name '" + name + "'."); + lines.add(" "); + lines.add(" Changes in this version:"); + lines.add(" - " + dataSource + " mutated e-mail address and name."); + lines.add(" "); + lines.add(""); + return new GitChangeSet(lines, authorOrCommitter); + } + + private GitChangeSet createAuthorChangeSet(String authorName, String authorEmail) { + return createChangeSet(true, authorName, authorEmail); + } + + private GitChangeSet createCommitterChangeSet(String committerName, String committerEmail) { + return createChangeSet(false, committerName, committerEmail); + } + + private static final String DEGENERATE_EMAIL_ADDRESS = "@"; + + @Test + public void testFindOrCreateUserAuthorBadEmail() { + String authorName = "Bad Author Test 1"; + GitChangeSet changeSet = createAuthorChangeSet(authorName, DEGENERATE_EMAIL_ADDRESS); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(authorName, DEGENERATE_EMAIL_ADDRESS, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(null, DEGENERATE_EMAIL_ADDRESS, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser("", DEGENERATE_EMAIL_ADDRESS, false, false)); + } + + @Test + public void testFindOrCreateUserCommitterBadEmail() { + String committerName = "Bad Committer Test 2"; + GitChangeSet changeSet = createCommitterChangeSet(committerName, DEGENERATE_EMAIL_ADDRESS); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(committerName, DEGENERATE_EMAIL_ADDRESS, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(null, DEGENERATE_EMAIL_ADDRESS, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser("", DEGENERATE_EMAIL_ADDRESS, false, false)); + } + + @Test + public void testFindOrCreateUserEmptyAuthor() { + String emptyAuthorName = ""; + String incompleteAuthorEmail = "@test3.example.com"; + GitChangeSet changeSet = createAuthorChangeSet(emptyAuthorName, incompleteAuthorEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(emptyAuthorName, incompleteAuthorEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(null, incompleteAuthorEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser("", incompleteAuthorEmail, false, false)); + } + + @Test + public void testFindOrCreateEmptyCommitter() { + String emptyCommitterName = ""; + String incompleteCommitterEmail = "@test4.example.com"; + GitChangeSet changeSet = createCommitterChangeSet(emptyCommitterName, incompleteCommitterEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(emptyCommitterName, incompleteCommitterEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(null, incompleteCommitterEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser("", incompleteCommitterEmail, false, false)); + } + + @Test + public void testFindOrCreateUserEmptyAuthorEmail() { + String authorName = "Author Test 5"; + String emptyAuthorEmail = ""; + GitChangeSet changeSet = createAuthorChangeSet(authorName, emptyAuthorEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(authorName, emptyAuthorEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(authorName, emptyAuthorEmail, true, false)); + } + + @Test + public void testFindOrCreateUserNullAuthorEmail() { + String authorName = "Author Test 6"; + String emptyAuthorEmail = ""; + GitChangeSet changeSet = createAuthorChangeSet(authorName, emptyAuthorEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(authorName, null, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(authorName, null, true, false)); + } + + @Test + public void testFindOrCreateUserEmptyCommitterEmail() { + String committerName = "Committer Test 7"; + String emptyCommitterEmail = ""; + GitChangeSet changeSet = createCommitterChangeSet(committerName, emptyCommitterEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(committerName, emptyCommitterEmail, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(committerName, emptyCommitterEmail, true, false)); + } + + @Test + public void testFindOrCreateUserNullCommitterEmail() { + String committerName = "Committer Test 8"; + String emptyCommitterEmail = ""; + GitChangeSet changeSet = createCommitterChangeSet(committerName, emptyCommitterEmail); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(committerName, null, false, false)); + assertEquals(User.getUnknown(), changeSet.findOrCreateUser(committerName, null, true, false)); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetBasicTest.java b/src/test/java/hudson/plugins/git/GitChangeSetBasicTest.java new file mode 100644 index 0000000000..ef97a4720c --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetBasicTest.java @@ -0,0 +1,252 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Arrays; +import static hudson.plugins.git.GitChangeSet.TRUNCATE_LIMIT; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Test; + +public class GitChangeSetBasicTest { + + private GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat) { + return GitChangeSetUtil.genChangeSet(authorOrCommitter, useLegacyFormat, true); + } + + private GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat, boolean hasParent) { + return GitChangeSetUtil.genChangeSet(authorOrCommitter, useLegacyFormat, hasParent); + } + + @Test + public void testLegacyChangeSet() { + GitChangeSet gitChangeSet = GitChangeSetUtil.genChangeSet(false, true, false, GitChangeSetUtil.COMMIT_TITLE ,false); + GitChangeSetUtil.assertChangeSet( gitChangeSet ); + } + + @Test + public void testChangeSet() { + GitChangeSetUtil.assertChangeSet(genChangeSet(false, false)); + } + + @Test + public void testChangeSetNoParent() { + GitChangeSet changeSet = genChangeSet(false, false, false); + GitChangeSetUtil.assertChangeSet(changeSet); + assertNull(changeSet.getParentCommit()); + } + + @Test + public void testCommitter() { + assertEquals(GitChangeSetUtil.COMMITTER_NAME, genChangeSet(false, false).getAuthorName()); + assertEquals(GitChangeSetUtil.COMMITTER_EMAIL, genChangeSet(false, false).getAuthorEmail()); + } + + @Test + public void testAuthor() { + assertEquals(GitChangeSetUtil.AUTHOR_NAME, genChangeSet(true, false).getAuthorName()); + assertEquals(GitChangeSetUtil.AUTHOR_EMAIL, genChangeSet(true, false).getAuthorEmail()); + } + + @Test + public void testGetDate() { + assertEquals("1970-01-15T06:56:08-0600", genChangeSet(true, false).getDate()); + } + + @Test + public void testGetTimestamp() { + assertEquals(1256168000L, genChangeSet(true, false).getTimestamp()); + } + + @Test + public void testInvalidDate() { + final String badDateString = "2015-03-03x09:22:42 -0700"; + GitChangeSet c = new GitChangeSet(Arrays.asList("author John Doe " + badDateString), true); + assertEquals(badDateString, c.getDate()); + assertEquals(-1L, c.getTimestamp()); + } + + @Test + public void testIsoDate() { + + GitChangeSet c = new GitChangeSet(Arrays.asList("author John Doe 2015-03-03T09:22:42-0700"), true); + assertEquals("2015-03-03T09:22:42-0700", c.getDate()); + assertEquals(1425399762000L, c.getTimestamp()); + + c = new GitChangeSet(Arrays.asList("author John Doe 2015-03-03T09:22:42-07:00"), true); + assertEquals("2015-03-03T09:22:42-07:00", c.getDate()); + assertEquals(1425399762000L, c.getTimestamp()); + + c = new GitChangeSet(Arrays.asList("author John Doe 2015-03-03T16:22:42Z"), true); + assertEquals("2015-03-03T16:22:42Z", c.getDate()); + assertEquals(1425399762000L, c.getTimestamp()); + + c = new GitChangeSet(Arrays.asList("author John Doe 1425399762"), true); + assertEquals("2015-03-03T16:22:42Z", c.getDate()); + assertEquals(1425399762000L, c.getTimestamp()); + + c = new GitChangeSet(Arrays.asList("author John Doe 1425374562 -0700"), true); + assertEquals("2015-03-03T09:22:42-0700", c.getDate()); + assertEquals(1425399762000L, c.getTimestamp()); + } + + private GitChangeSet genChangeSetForSwedCase(boolean authorOrCommitter) { + ArrayList lines = new ArrayList<>(); + lines.add("commit 1567861636cd854f4dd6fa40bf94c0c657681dd5"); + lines.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); + lines.add("parent e74a24e995305bd67a180f0ebc57927e2b8783ce"); + lines.add("author misterÅ 1363879004 +0100"); + lines.add("committer Mister Åhlander 1364199539 -0400"); + lines.add(""); + lines.add(" [task] Updated version."); + lines.add(" "); + lines.add(" Including earlier updates."); + lines.add(" "); + lines.add(" Changes in this version:"); + lines.add(" - Changed to take the gerrit url from gerrit query command."); + lines.add(" - Aligned reason information with our new commit hooks"); + lines.add(" "); + lines.add(" Change-Id: Ife96d2abed5b066d9620034bec5f04cf74b8c66d"); + lines.add(" Reviewed-on: https://gerrit.e.se/12345"); + lines.add(" Tested-by: Jenkins "); + lines.add(" Reviewed-by: Mister Another "); + lines.add(""); + //above lines all on purpose vs specific troublesome case @ericsson. + return new GitChangeSet(lines, authorOrCommitter); + } + + @Test + public void testSwedishCommitterName() { + assertEquals("Mister Åhlander", genChangeSetForSwedCase(false).getAuthorName()); + } + + @Test + public void testSwedishAuthorName() { + assertEquals("misterÅ", genChangeSetForSwedCase(true).getAuthorName()); + } + + @Test + public void testSwedishDate() { + assertEquals("2013-03-21T15:16:44+0100", genChangeSetForSwedCase(true).getDate()); + } + + @Test + public void testSwedishTimestamp() { + assertEquals(1363875404000L, genChangeSetForSwedCase(true).getTimestamp()); + } + + @Test + public void testChangeLogTruncationWithShortMessage(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "Lorem ipsum dolor sit amet.", + false); + String msg = changeSet.getMsg(); + assertThat("Title is correct ", msg, containsString("Lorem ipsum dolor sit amet.") ); + assertThat("Title length is correct ", msg.length(), lessThanOrEqualTo(TRUNCATE_LIMIT)); + } + + @Test + public void testChangeLogTruncationWithNewLine(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "Lorem ipsum dolor sit amet, "+System.lineSeparator()+"consectetur adipiscing elit.", + false); + String msg = changeSet.getMsg(); + assertThat(msg, is("Lorem ipsum dolor sit amet,")); + assertThat("Title length is correct ", msg.length(), lessThanOrEqualTo(TRUNCATE_LIMIT)); + } + + @Test + public void testChangeLogRetainSummaryWithoutNewLine(){ + String originalCommitMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pellentesque ipsum non aliquam interdum. Integer metus orci, vulputate id turpis in, pharetra pretium magna. Fusce sollicitudin vehicula lectus. Nam ut eros purus. Mauris aliquam mi et nunc porta, non consectetur mauris pretium. Fusce a venenatis dolor. Sed commodo, dui ac posuere dignissim, dolor tortor semper eros, varius consequat nulla purus a lacus. Vestibulum egestas, orci vitae pellentesque laoreet, dolor lorem molestie tellus, nec luctus lorem ex quis orci. Phasellus interdum elementum luctus. Nam commodo, turpis in sollicitudin auctor, ipsum lectus finibus erat, in iaculis sapien neque ultrices sapien. In congue diam semper tortor laoreet aliquet. Mauris lacinia quis nunc vel accumsan. Nullam sed nisl eget orci porttitor venenatis. Lorem ipsum dolor sit amet, consectetur adipiscing elit"; + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + originalCommitMessage, + true); + assertThat(changeSet.getMsg(), is(originalCommitMessage)); + } + + @Test + public void testChangeLogDoNotRetainSummaryWithoutNewLine(){ + String msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pellentesque ipsum non aliquam interdum. Integer metus orci, vulputate id turpis in, pharetra pretium magna. Fusce sollicitudin vehicula lectus. Nam ut eros purus. Mauris aliquam mi et nunc porta, non consectetur mauris pretium. Fusce a venenatis dolor. Sed commodo, dui ac posuere dignissim, dolor tortor semper eros, varius consequat nulla purus a lacus. Vestibulum egestas, orci vitae pellentesque laoreet, dolor lorem molestie tellus, nec luctus lorem ex quis orci. Phasellus interdum elementum luctus. Nam commodo, turpis in sollicitudin auctor, ipsum lectus finibus erat, in iaculis sapien neque ultrices sapien. In congue diam semper tortor laoreet aliquet. Mauris lacinia quis nunc vel accumsan. Nullam sed nisl eget orci porttitor venenatis. Lorem ipsum dolor sit amet, consectetur adipiscing elit"; + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + msg, + false); + assertThat(changeSet.getMsg(), is("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus")); + } + + @Test + public void testChangeLogNoTruncationWithNewLine(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "Lorem ipsum dolor sit amet, consectetur "+System.lineSeparator()+" adipiscing elit. Phasellus pellentesque ipsum non aliquam interdum. Integer metus orci, vulputate id turpis in, pharetra pretium magna. Fusce sollicitudin vehicula lectus. Nam ut eros purus. Mauris aliquam mi et nunc porta, non consectetur mauris pretium. Fusce a venenatis dolor. Sed commodo, dui ac posuere dignissim, dolor tortor semper eros, varius consequat nulla purus a lacus. Vestibulum egestas, orci vitae pellentesque laoreet, dolor lorem molestie tellus, nec luctus lorem ex quis orci. Phasellus interdum elementum luctus. Nam commodo, turpis in sollicitudin auctor, ipsum lectus finibus erat, in iaculis sapien neque ultrices sapien. In congue diam semper tortor laoreet aliquet. Mauris lacinia quis nunc vel accumsan. Nullam sed nisl eget orci porttitor venenatis. Lorem ipsum dolor sit amet, consectetur adipiscing elit", + true); + String msg = changeSet.getMsg(); + assertThat("Title is correct ", msg, is("Lorem ipsum dolor sit amet, consectetur") ); + } + + @Test + public void testChangeLogEdgeCaseNotTruncating(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "[JENKINS-012345] 8901 34567 90 23456 8901 34567 9012 4567890 2345678 0 2 4 5", + false); + String msg = changeSet.getMsg(); + assertThat( msg.length(), lessThanOrEqualTo( TRUNCATE_LIMIT )); + assertThat( msg, is("[JENKINS-012345] 8901 34567 90 23456 8901 34567 9012 4567890 2345678 0 2") ); + } + + @Test + public void testChangeLogEdgeCaseTruncating(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "[JENKINS-012345] 8901 34567 90 23456 8901 34567 9012 4567890 2345678 0 2 4 5", + true); + String msg = changeSet.getMsg(); + assertThat( msg, is("[JENKINS-012345] 8901 34567 90 23456 8901 34567 9012 4567890 2345678 0 2 4 5") ); + } + + @Test + public void testChangeLogEdgeCaseTruncatingAndNewLine(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "[JENKINS-012345] 8901 34567 " + System.lineSeparator() + "90 23456 8901 34567 9012 4567890 2345678 0 2 4 5", + true); + String msg = changeSet.getMsg(); + assertThat( msg, is("[JENKINS-012345] 8901 34567") ); + } + + @Test + public void testLongString(){ + GitChangeSet changeSet = GitChangeSetUtil.genChangeSet(true, false, true, + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + false); + String msg = changeSet.getMsg(); + assertThat( msg, is("12345678901234567890123456789012345678901234567890123456789012345678901234567890") ); + } + + @Test + public void stringSplitter(){ + String msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pellentesque ipsum non aliquam interdum. Integer metus orci, vulputate id turpis in, pharetra pretium magna. Fusce sollicitudin vehicula lectus. Nam ut eros purus. Mauris aliquam mi et nunc porta, non consectetur mauris pretium. Fusce a venenatis dolor. Sed commodo, dui ac posuere dignissim, dolor tortor semper eros, varius consequat nulla purus a lacus. Vestibulum egestas, orci vitae pellentesque laoreet, dolor lorem molestie tellus, nec luctus lorem ex quis orci. Phasellus interdum elementum luctus. Nam commodo, turpis in sollicitudin auctor, ipsum lectus finibus erat, in iaculis sapien neque ultrices sapien. In congue diam semper tortor laoreet aliquet. Mauris lacinia quis nunc vel accumsan. Nullam sed nisl eget orci porttitor venenatis. Lorem ipsum dolor sit amet, consectetur adipiscing elit"; + assertThat(GitChangeSet.splitString(msg, 15), is("Lorem ipsum")); + assertThat(GitChangeSet.splitString(msg, 16), is("Lorem ipsum")); + assertThat(GitChangeSet.splitString(msg, 17), is("Lorem ipsum dolor")); + assertThat(GitChangeSet.splitString(msg, 18), is("Lorem ipsum dolor")); + assertThat(GitChangeSet.splitString(msg, 19), is("Lorem ipsum dolor")); + assertThat(GitChangeSet.splitString(msg, 20), is("Lorem ipsum dolor")); + assertThat(GitChangeSet.splitString(msg, 21), is("Lorem ipsum dolor sit")); + assertThat(GitChangeSet.splitString(msg, 22), is("Lorem ipsum dolor sit")); + + msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pellentesque ipsum non aliquam interdum."; + assertThat(GitChangeSet.splitString(msg, TRUNCATE_LIMIT), + is("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus")); + } + + @Test + public void splitingWithBrackets(){ + assertThat(GitChangeSet.splitString("[task] Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 25), is("[task] Lorem ipsum dolor")); + } + + @Test + public void splitingEmptyString(){ + assertThat(GitChangeSet.splitString("", 25), is("")); + } + + @Test + public void splitingNullString(){ + assertThat(GitChangeSet.splitString(null, 25), is("")); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetEmptyTest.java b/src/test/java/hudson/plugins/git/GitChangeSetEmptyTest.java new file mode 100644 index 0000000000..b813156066 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetEmptyTest.java @@ -0,0 +1,102 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class GitChangeSetEmptyTest { + + private GitChangeSet changeSet = null; + + public GitChangeSetEmptyTest() { + } + + @Before + public void createEmptyChangeSet() { + changeSet = new GitChangeSet(new ArrayList<>(), false); + } + + @Test + public void testGetDate() { + assertNull(changeSet.getDate()); + } + + @Test + public void testGetTimestamp() { + assertEquals(-1L, changeSet.getTimestamp()); + } + + @Test + public void testGetCommitId() { + assertNull(changeSet.getCommitId()); + } + + @Test + public void testSetParent() { + changeSet.setParent(null); + assertNull(changeSet.getParent()); + } + + @Test + public void testGetParentCommit() { + assertNull(changeSet.getParentCommit()); + } + + @Test + public void testGetAffectedPaths() { + assertTrue(changeSet.getAffectedPaths().isEmpty()); + } + + @Test + public void testGetPaths() { + assertTrue(changeSet.getPaths().isEmpty()); + } + + @Test + public void testGetAffectedFiles() { + assertTrue(changeSet.getAffectedFiles().isEmpty()); + } + + @Test + public void testGetAuthorName() { + assertNull(changeSet.getAuthorName()); + } + + @Test + public void testGetMsg() { + assertNull(changeSet.getMsg()); + } + + @Test + public void testGetId() { + assertNull(changeSet.getId()); + } + + @Test + public void testGetRevision() { + assertNull(changeSet.getRevision()); + } + + @Test + public void testGetComment() { + assertNull(changeSet.getComment()); + } + + @Test + public void testGetBranch() { + assertNull(changeSet.getBranch()); + } + + @Test + public void testHashCode() { + assertTrue(changeSet.hashCode() != 0); + } + + @Test + public void testEquals() { + assertEquals(changeSet, changeSet); + assertNotEquals(changeSet, GitChangeSetUtil.genChangeSet(true, true)); + } + +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetEuroTest.java b/src/test/java/hudson/plugins/git/GitChangeSetEuroTest.java new file mode 100644 index 0000000000..77e78c5477 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetEuroTest.java @@ -0,0 +1,156 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitChangeSetEuroTest { + + private final String id = "1567861636cd854f4dd6fa40bf94c0c657681dd5"; + private final String parent = "e74a24e995305bd67a180f0ebc57927e2b8783ce"; + private final String authorName = "Mr. Åhłañder"; + private final String committerName = "Mister Åhländèr"; + private final String msg = "[task] Updated version."; + private final String comment1 = "Including earlier updates."; + private final String commentStartText = msg + "\n\n" + comment1 + "\n"; + + private GitChangeSet changeSet = null; + private final boolean useAuthorName; + + public GitChangeSetEuroTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorNameAndLegacyLayout() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Before + public void createEuroChangeSet() { + ArrayList gitChangeLog = new ArrayList<>(); + gitChangeLog.add("commit " + id); + gitChangeLog.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); + gitChangeLog.add("parent " + parent); + gitChangeLog.add("author " + authorName + " 1363879004 +0100"); + gitChangeLog.add("committer " + committerName + " 1364199539 -0400"); + gitChangeLog.add(""); + gitChangeLog.add(" " + msg); + gitChangeLog.add(" "); + gitChangeLog.add(" " + comment1); + gitChangeLog.add(" "); + gitChangeLog.add(" Changes in this version:"); + gitChangeLog.add(" - Changed to take the gerrit url from gerrit query command."); + gitChangeLog.add(" - Aligned reason information with our new commit hooks"); + gitChangeLog.add(" "); + gitChangeLog.add(" Change-Id: Ife96d2abed5b066d9620034bec5f04cf74b8c66d"); + gitChangeLog.add(" Reviewed-on: https://gerrit.e.se/12345"); + gitChangeLog.add(" Tested-by: Jenkins "); + gitChangeLog.add(" Reviewed-by: Mister Another "); + gitChangeLog.add(""); + changeSet = new GitChangeSet(gitChangeLog, useAuthorName, false); + } + + @Test + public void testGetCommitId() { + assertEquals(id, changeSet.getCommitId()); + } + + @Test + public void testSetParent() { + changeSet.setParent(null); + assertNull(changeSet.getParent()); + } + + @Test + public void testGetParentCommit() { + assertEquals(parent, changeSet.getParentCommit()); + } + + @Test + public void testGetAffectedPaths() { + assertTrue(changeSet.getAffectedPaths().isEmpty()); + } + + @Test + public void testGetPaths() { + assertTrue(changeSet.getPaths().isEmpty()); + } + + @Test + public void testGetAffectedFiles() { + assertTrue(changeSet.getAffectedFiles().isEmpty()); + } + + @Test + public void testGetAuthorName() { + assertEquals(useAuthorName ? authorName : committerName, changeSet.getAuthorName()); + } + + @Test + public void testGetMsg() { + assertEquals(msg, changeSet.getMsg()); + } + + @Test + public void testGetId() { + assertEquals(id, changeSet.getId()); + } + + @Test + public void testGetRevision() { + assertEquals(id, changeSet.getRevision()); + } + + @Test + public void testGetComment() { + assertTrue(changeSet.getComment().startsWith(commentStartText)); + } + + @Test + public void testGetBranch() { + assertNull(changeSet.getBranch()); + } + + @Test + public void testGetDate() { + assertEquals(useAuthorName ? "2013-03-21T15:16:44+0100" : "2013-03-25T08:18:59-0400", changeSet.getDate()); + } + + @Test + public void testGetTimestamp() { + assertEquals(useAuthorName ? 1363875404000L : 1364213939000L, changeSet.getTimestamp()); + } + + @Test + public void testHashCode() { + assertTrue(changeSet.hashCode() != 0); + } + + @Test + public void testEquals() { + assertEquals(changeSet, changeSet); + + assertEquals(GitChangeSetUtil.genChangeSet(false, false), GitChangeSetUtil.genChangeSet(false, false)); + assertEquals(GitChangeSetUtil.genChangeSet(true, false), GitChangeSetUtil.genChangeSet(true, false)); + assertEquals(GitChangeSetUtil.genChangeSet(false, true), GitChangeSetUtil.genChangeSet(false, true)); + assertEquals(GitChangeSetUtil.genChangeSet(true, true), GitChangeSetUtil.genChangeSet(true, true)); + + assertNotEquals(changeSet, GitChangeSetUtil.genChangeSet(false, false)); + assertNotEquals(GitChangeSetUtil.genChangeSet(true, false), changeSet); + assertNotEquals(changeSet, GitChangeSetUtil.genChangeSet(false, true)); + assertNotEquals(GitChangeSetUtil.genChangeSet(true, true), changeSet); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetListTest.java b/src/test/java/hudson/plugins/git/GitChangeSetListTest.java new file mode 100644 index 0000000000..f60f2615e5 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetListTest.java @@ -0,0 +1,102 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.model.Run; +import hudson.scm.RepositoryBrowser; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; + +import org.junit.Test; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Before; + +public class GitChangeSetListTest { + + private final GitChangeSetList emptyChangeSetList; + private GitChangeSetList changeSetList; + private GitChangeSet changeSet; + + public GitChangeSetListTest() { + RepositoryBrowser browser = null; + Run build = null; + emptyChangeSetList = new GitChangeSetList(build, browser, new ArrayList<>()); + } + + @Before + public void createGitChangeSetList() { + RepositoryBrowser browser = null; + Run build = null; + List logs = new ArrayList<>(); + List changeSetText = new ArrayList<>(); + changeSet = new GitChangeSet(changeSetText, true); + assertTrue(logs.add(changeSet)); + assertThat(changeSet.getParent(), is(nullValue())); + changeSetList = new GitChangeSetList(build, browser, logs); + assertThat(changeSet.getParent(), is(changeSetList)); + } + + @Test + public void testIsEmptySet() { + assertFalse(changeSetList.isEmptySet()); + } + + @Test + public void testIsEmptySetReallyEmpty() { + assertTrue(emptyChangeSetList.isEmptySet()); + } + + @Test + public void testIterator() { + Iterator iterator = changeSetList.iterator(); + GitChangeSet firstChangeSet = iterator.next(); + assertThat(firstChangeSet, is(changeSet)); + assertFalse(iterator.hasNext()); + } + + @Test + public void testIteratorReallyE() { + Iterator iterator = emptyChangeSetList.iterator(); + assertFalse(iterator.hasNext()); + } + + @Test + public void testGetLogs() { + List result = changeSetList.getLogs(); + assertThat(result, contains(changeSet)); + } + + @Test + public void testGetLogsReallyEmpty() { + List result = emptyChangeSetList.getLogs(); + assertThat(result, is(empty())); + } + + @Test + public void testGetKind() { + assertThat(changeSetList.getKind(), is("git")); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetPluginHistoryTest.java b/src/test/java/hudson/plugins/git/GitChangeSetPluginHistoryTest.java new file mode 100644 index 0000000000..5ca341bed3 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetPluginHistoryTest.java @@ -0,0 +1,145 @@ +package hudson.plugins.git; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import org.eclipse.jgit.lib.ObjectId; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jenkins.plugins.git.GitSampleRepoRule; + +@RunWith(Parameterized.class) +public class GitChangeSetPluginHistoryTest { + + private static final long FIRST_COMMIT_TIMESTAMP = 1198029565000L; + private static final long NOW = System.currentTimeMillis(); + + private final GitClient git; + private final boolean authorOrCommitter; + private final ObjectId sha1; + + private final GitChangeSet changeSet; + + @ClassRule + public static GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + /* git 1.7.1 on CentOS 6.7 "whatchanged" generates no output for + * the SHA1 hashes (from this repository) in this list. Rather + * than skip testing on that old git version, this exclusion list + * allows most tests to run. Debian 6 / git 1.7.2.5 also has the issue. + */ + private static final String[] git171exceptions = { + "6e467b23", + "750b6806", + "7eeb070b", + "87988f4d", + "94d982c2", + "a571899e", + "b9e497b0", + "bc71cd2d", + "bca98ea9", + "c73b4ff3", + "dcd329f4", + "edf066f3", + }; + + public GitChangeSetPluginHistoryTest(GitClient git, boolean authorOrCommitter, String sha1String) throws IOException, InterruptedException { + this.git = git; + this.authorOrCommitter = authorOrCommitter; + this.sha1 = ObjectId.fromString(sha1String); + StringWriter stringWriter = new StringWriter(); + git.changelog().includes(sha1).max(1).to(stringWriter).execute(); + List changeLogStrings = new ArrayList<>(Arrays.asList(stringWriter.toString().split("\n"))); + changeSet = new GitChangeSet(changeLogStrings, authorOrCommitter); + } + + /** + * Merge changes won't compute their date in GitChangeSet, apparently as an + * intentional design choice. Return all changes for this repository which + * are not merges. + * + * @return ObjectId list for all changes which aren't merges + */ + private static List getNonMergeChanges(boolean honorExclusions) throws IOException { + List nonMergeChanges = new ArrayList<>(); + Process process = new ProcessBuilder("git", "rev-list", "--no-merges", "HEAD").start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (honorExclusions) { + boolean ignore = false; + for (String exclusion : git171exceptions) { + if (line.startsWith(exclusion)) { + ignore = true; + break; + } + } + if (!ignore) { + nonMergeChanges.add(ObjectId.fromString(line)); + } + } else { + nonMergeChanges.add(ObjectId.fromString(line)); + } + } + } + process.destroy(); + Collections.shuffle(nonMergeChanges); + return nonMergeChanges; + } + + @Parameterized.Parameters(name = "{2}-{1}") + public static Collection generateData() throws IOException, InterruptedException { + List args = new ArrayList<>(); + String[] implementations = new String[]{"git", "jgit"}; + boolean[] choices = {true, false}; + + for (final String implementation : implementations) { + EnvVars envVars = new EnvVars(); + TaskListener listener = StreamTaskListener.fromStdout(); + GitClient git = Git.with(listener, envVars).in(new FilePath(new File("."))).using(implementation).getClient(); + boolean honorExclusions = implementation.equals("git") && !sampleRepo.gitVersionAtLeast(1, 7, 10); + List allNonMergeChanges = getNonMergeChanges(honorExclusions); + int count = allNonMergeChanges.size() / 10; /* 10% of all changes */ + + for (boolean authorOrCommitter : choices) { + for (int index = 0; index < count; index++) { + ObjectId sha1 = allNonMergeChanges.get(index); + Object[] argList = {git, authorOrCommitter, sha1.getName()}; + args.add(argList); + } + } + } + return args; + } + + @Test + public void timestampInRange() { + long timestamp = changeSet.getTimestamp(); + assertThat(timestamp, is(greaterThanOrEqualTo(FIRST_COMMIT_TIMESTAMP))); + assertThat(timestamp, is(lessThan(NOW))); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetSimpleTest.java b/src/test/java/hudson/plugins/git/GitChangeSetSimpleTest.java new file mode 100644 index 0000000000..73a6040116 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetSimpleTest.java @@ -0,0 +1,181 @@ +package hudson.plugins.git; + +import hudson.scm.EditType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitChangeSetSimpleTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private GitChangeSet changeSet = null; + private final boolean useAuthorName; + private final boolean useLegacyFormat; + + public GitChangeSetSimpleTest(String useAuthorName, String useLegacyFormat) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + this.useLegacyFormat = Boolean.valueOf(useLegacyFormat); + } + + @Parameterized.Parameters(name = "{0},{1}") + public static Collection permuteAuthorNameAndLegacyLayout() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + for (String legacyFormat : allowed) { + Object[] combination = {authorName, legacyFormat}; + values.add(combination); + } + } + return values; + } + + @Before + public void createSimpleChangeSet() { + changeSet = GitChangeSetUtil.genChangeSet(useAuthorName, useLegacyFormat); + } + + @Test + public void testChangeSetDetails() { + assertEquals(GitChangeSetUtil.ID, changeSet.getId()); + assertEquals(GitChangeSetUtil.COMMIT_TITLE, changeSet.getMsg()); + assertEquals("Commit title.\nCommit extended description.\n", changeSet.getComment()); + HashSet expectedAffectedPaths = new HashSet<>(7); + expectedAffectedPaths.add("src/test/add.file"); + expectedAffectedPaths.add("src/test/deleted.file"); + expectedAffectedPaths.add("src/test/modified.file"); + expectedAffectedPaths.add("src/test/renamedFrom.file"); + expectedAffectedPaths.add("src/test/renamedTo.file"); + expectedAffectedPaths.add("src/test/copyOf.file"); + assertEquals(expectedAffectedPaths, changeSet.getAffectedPaths()); + + Collection actualPaths = changeSet.getPaths(); + assertEquals(6, actualPaths.size()); + for (GitChangeSet.Path path : actualPaths) { + if (null != path.getPath()) switch (path.getPath()) { + case "src/test/add.file": + assertEquals(EditType.ADD, path.getEditType()); + assertNull(path.getSrc()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); + break; + case "src/test/deleted.file": + assertEquals(EditType.DELETE, path.getEditType()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + assertNull(path.getDst()); + break; + case "src/test/modified.file": + assertEquals(EditType.EDIT, path.getEditType()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/renamedFrom.file": + assertEquals(EditType.DELETE, path.getEditType()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/renamedTo.file": + assertEquals(EditType.ADD, path.getEditType()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/copyOf.file": + assertEquals(EditType.ADD, path.getEditType()); + assertEquals("bc234def567abc890def123abc456def789abc01", path.getSrc()); + assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); + break; + default: + fail("Unrecognized path."); + break; + } + } + } + + @Test + public void testGetCommitId() { + assertEquals(GitChangeSetUtil.ID, changeSet.getCommitId()); + } + + @Test + public void testSetParent() { + changeSet.setParent(null); + assertNull(changeSet.getParent()); + } + + @Test + public void testGetParentCommit() { + assertEquals(GitChangeSetUtil.PARENT, changeSet.getParentCommit()); + } + + @Test + public void testGetAffectedFiles() { + assertEquals(6, changeSet.getAffectedFiles().size()); + } + + @Test + public void testGetAuthorName() { + assertEquals(useAuthorName ? GitChangeSetUtil.AUTHOR_NAME : GitChangeSetUtil.COMMITTER_NAME, changeSet.getAuthorName()); + } + + @Test + public void testGetDate() { + assertEquals(useAuthorName ? GitChangeSetUtil.AUTHOR_DATE_FORMATTED : GitChangeSetUtil.COMMITTER_DATE_FORMATTED, changeSet.getDate()); + } + + @Test + public void testGetMsg() { + assertEquals(GitChangeSetUtil.COMMIT_TITLE, changeSet.getMsg()); + } + + @Test + public void testGetId() { + assertEquals(GitChangeSetUtil.ID, changeSet.getId()); + } + + @Test + public void testGetRevision() { + assertEquals(GitChangeSetUtil.ID, changeSet.getRevision()); + } + + @Test + public void testGetComment() { + String changeComment = changeSet.getComment(); + assertTrue("Comment '" + changeComment + "' does not start with '" + GitChangeSetUtil.COMMENT + "'", changeComment.startsWith(GitChangeSetUtil.COMMENT)); + } + + @Test + public void testGetBranch() { + assertNull(changeSet.getBranch()); + } + + @Test + public void testHashCode() { + assertTrue(changeSet.hashCode() != 0); + } + + @Test + public void testEquals() { + assertTrue(changeSet.equals(changeSet)); + assertFalse(changeSet.equals(new GitChangeSet(new ArrayList<>(), false))); + } + + @Test + public void testChangeSetExceptionMessage() { + final String expectedLineContent = "commit "; + ArrayList lines = new ArrayList<>(); + lines.add(expectedLineContent); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Commit has no ID[" + expectedLineContent + "]"); + GitChangeSet badChangeSet = new GitChangeSet(lines, true); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetTest.java b/src/test/java/hudson/plugins/git/GitChangeSetTest.java index af5cbaa89f..d1ae260f6d 100644 --- a/src/test/java/hudson/plugins/git/GitChangeSetTest.java +++ b/src/test/java/hudson/plugins/git/GitChangeSetTest.java @@ -1,167 +1,78 @@ package hudson.plugins.git; import hudson.model.User; -import hudson.plugins.git.GitChangeSet.Path; -import hudson.scm.EditType; import hudson.tasks.Mailer; import hudson.tasks.Mailer.UserProperty; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; +import java.io.IOException; -import org.jvnet.hudson.test.HudsonTestCase; +public class GitChangeSetTest { -import junit.framework.Assert; + @Rule + public JenkinsRule jenkins = new JenkinsRule(); -public class GitChangeSetTest extends HudsonTestCase { - - public GitChangeSetTest(String testName) { - super(testName); - } + @Test + public void testFindOrCreateUser() { + final GitChangeSet committerCS = GitChangeSetUtil.genChangeSet(false, false); + final String email = "jauthor@nospam.com"; + final boolean createAccountBasedOnEmail = true; + final boolean useExistingAccountBasedOnEmail = false; - private GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat) { - ArrayList lines = new ArrayList(); - lines.add("Some header junk we should ignore..."); - lines.add("header line 2"); - lines.add("commit 123abc456def"); - lines.add("tree 789ghi012jkl"); - lines.add("parent 345mno678pqr"); - lines.add("author John Author 1234567 -0600"); - lines.add("committer John Committer 1234567 -0600"); - lines.add(""); - lines.add(" Commit title."); - lines.add(" Commit extended description."); - lines.add(""); - if (useLegacyFormat) { - lines.add("123abc456def"); - lines.add(" create mode 100644 some/file1"); - lines.add(" delete mode 100644 other/file2"); - } - lines.add(":000000 123456 0000000000000000000000000000000000000000 123abc456def789abc012def345abc678def901a A\tsrc/test/add.file"); - lines.add(":123456 000000 123abc456def789abc012def345abc678def901a 0000000000000000000000000000000000000000 D\tsrc/test/deleted.file"); - lines.add(":123456 789012 123abc456def789abc012def345abc678def901a bc234def567abc890def123abc456def789abc01 M\tsrc/test/modified.file"); - lines.add(":123456 789012 123abc456def789abc012def345abc678def901a bc234def567abc890def123abc456def789abc01 R012\tsrc/test/renamedFrom.file\tsrc/test/renamedTo.file"); - lines.add(":000000 123456 bc234def567abc890def123abc456def789abc01 123abc456def789abc012def345abc678def901a C100\tsrc/test/original.file\tsrc/test/copyOf.file"); - - return new GitChangeSet(lines, authorOrCommitter); - } - - public void testLegacyChangeSet() { - GitChangeSet changeSet = genChangeSet(false, true); - assertChangeSet(changeSet); - } - - public void testChangeSet() { - GitChangeSet changeSet = genChangeSet(false, false); - assertChangeSet(changeSet); - } + User user = committerCS.findOrCreateUser(GitChangeSetUtil.AUTHOR_NAME, email, createAccountBasedOnEmail, useExistingAccountBasedOnEmail); + assertNotNull(user); - private void assertChangeSet(GitChangeSet changeSet) { - Assert.assertEquals("123abc456def", changeSet.getId()); - Assert.assertEquals("Commit title.", changeSet.getMsg()); - Assert.assertEquals("Commit title.\nCommit extended description.\n", changeSet.getComment()); - HashSet expectedAffectedPaths = new HashSet(7); - expectedAffectedPaths.add("src/test/add.file"); - expectedAffectedPaths.add("src/test/deleted.file"); - expectedAffectedPaths.add("src/test/modified.file"); - expectedAffectedPaths.add("src/test/renamedFrom.file"); - expectedAffectedPaths.add("src/test/renamedTo.file"); - expectedAffectedPaths.add("src/test/copyOf.file"); - Assert.assertEquals(expectedAffectedPaths, changeSet.getAffectedPaths()); - - Collection actualPaths = changeSet.getPaths(); - Assert.assertEquals(6, actualPaths.size()); - for (Path path : actualPaths) { - if ("src/test/add.file".equals(path.getPath())) { - Assert.assertEquals(EditType.ADD, path.getEditType()); - Assert.assertNull(path.getSrc()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); - } else if ("src/test/deleted.file".equals(path.getPath())) { - Assert.assertEquals(EditType.DELETE, path.getEditType()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); - Assert.assertNull(path.getDst()); - } else if ("src/test/modified.file".equals(path.getPath())) { - Assert.assertEquals(EditType.EDIT, path.getEditType()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); - Assert.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); - } else if ("src/test/renamedFrom.file".equals(path.getPath())) { - Assert.assertEquals(EditType.DELETE, path.getEditType()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); - Assert.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); - } else if ("src/test/renamedTo.file".equals(path.getPath())) { - Assert.assertEquals(EditType.ADD, path.getEditType()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); - Assert.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); - } else if ("src/test/copyOf.file".equals(path.getPath())) { - Assert.assertEquals(EditType.ADD, path.getEditType()); - Assert.assertEquals("bc234def567abc890def123abc456def789abc01", path.getSrc()); - Assert.assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); - } else { - Assert.fail("Unrecognized path."); - } - } - } - - public void testAuthorOrCommitter() { - GitChangeSet committerCS = genChangeSet(false, false); - - Assert.assertEquals("John Committer", committerCS.getAuthorName()); - - GitChangeSet authorCS = genChangeSet(true, false); - - Assert.assertEquals("John Author", authorCS.getAuthorName()); - } - - public void testFindOrCreateUser() { - GitChangeSet committerCS = genChangeSet(false, false); - String csAuthor = "John Author"; - String csAuthorEmail = "jauthor@nospam.com"; - boolean createAccountBasedOnEmail = true; - - User user = committerCS.findOrCreateUser(csAuthor, csAuthorEmail, createAccountBasedOnEmail); - Assert.assertNotNull(user); - - UserProperty property = user.getProperty(Mailer.UserProperty.class); - Assert.assertNotNull(property); - - String address = property.getAddress(); - Assert.assertNotNull(address); - Assert.assertEquals(csAuthorEmail, address); - } + UserProperty property = user.getProperty(Mailer.UserProperty.class); + assertNotNull(property); + + String address = property.getAddress(); + assertNotNull(address); + assertEquals(email, address); - private GitChangeSet genChangeSetForSwedCase(boolean authorOrCommitter) { - ArrayList lines = new ArrayList(); - lines.add("commit 1567861636cd854f4dd6fa40bf94c0c657681dd5"); - lines.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); - lines.add("parent e74a24e995305bd67a180f0ebc57927e2b8783ce"); - lines.add("author mistera 1363879004 +0100"); - lines.add("committer Mister Åhlander 1364199539 -0400"); - lines.add(""); - lines.add(" [task] Updated version."); - lines.add(" "); - lines.add(" Including earlier updates."); - lines.add(" "); - lines.add(" Changes in this version:"); - lines.add(" - Changed to take the gerrit url from gerrit query command."); - lines.add(" - Aligned reason information with our new commit hooks"); - lines.add(" "); - lines.add(" Change-Id: Ife96d2abed5b066d9620034bec5f04cf74b8c66d"); - lines.add(" Reviewed-on: https://gerrit.e.se/12345"); - lines.add(" Tested-by: Jenkins "); - lines.add(" Reviewed-by: Mister Another "); - lines.add(""); - //above lines all on purpose vs specific troublesome case @ericsson. - return new GitChangeSet(lines, authorOrCommitter); + assertEquals(User.getUnknown(), committerCS.findOrCreateUser(null, email, false, useExistingAccountBasedOnEmail)); + assertEquals(User.getUnknown(), committerCS.findOrCreateUser(null, email, true, useExistingAccountBasedOnEmail)); } - public void testAuthorOrCommitterSwedCase() { - GitChangeSet committerCS = genChangeSetForSwedCase(false); + @Test + public void testFindOrCreateUserBasedOnExistingUsersEmail() throws IOException { + final GitChangeSet committerCS = GitChangeSetUtil.genChangeSet(true, false); + final String existingUserId = "An existing user"; + final String existingUserFullName = "Some FullName"; + final String email = "jcommitter@nospam.com"; + final boolean createAccountBasedOnEmail = true; + final boolean useExistingAccountBasedOnEmail = true; + + assertNull(User.get(email, false)); + + User existingUser = User.get(existingUserId, true); + existingUser.setFullName(existingUserFullName); + existingUser.addProperty(new Mailer.UserProperty(email)); - Assert.assertEquals("Mister Åhlander", committerCS.getAuthorName());//swedish char on purpose + User user = committerCS.findOrCreateUser(GitChangeSetUtil.COMMITTER_NAME, email, createAccountBasedOnEmail, useExistingAccountBasedOnEmail); + assertNotNull(user); + assertEquals(user.getId(), existingUserId); + assertEquals(user.getFullName(), existingUserFullName); - GitChangeSet authorCS = genChangeSetForSwedCase(true); + UserProperty property = user.getProperty(Mailer.UserProperty.class); + assertNotNull(property); + + String address = property.getAddress(); + assertNotNull(address); + assertEquals(email, address); + + assertEquals(User.getUnknown(), committerCS.findOrCreateUser(null, email, false, useExistingAccountBasedOnEmail)); + assertEquals(User.getUnknown(), committerCS.findOrCreateUser(null, email, true, useExistingAccountBasedOnEmail)); + } - Assert.assertEquals("mistera", authorCS.getAuthorName()); + @Test + public void findOrCreateByFullName() throws Exception { + GitChangeSet cs = GitChangeSetUtil.genChangeSet(false, false); + User user = User.get("john"); + user.setFullName(GitChangeSetUtil.COMMITTER_NAME); + user.addProperty(new Mailer.UserProperty(GitChangeSetUtil.COMMITTER_EMAIL)); + assertEquals(user, cs.getAuthor()); } } diff --git a/src/test/java/hudson/plugins/git/GitChangeSetTimestampTest.java b/src/test/java/hudson/plugins/git/GitChangeSetTimestampTest.java new file mode 100644 index 0000000000..8788d488c9 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetTimestampTest.java @@ -0,0 +1,101 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; + +/** + * JENKINS-30073 reports that the timestamp returns -1 for the typical timestamp + * reported by the +%ci format to git log and git whatchanged. This test + * duplicates the bug and tests many other date formatting cases. + * See JENKINS-55693 for more details on joda time replacement. + * + * @author Mark Waite + */ +@RunWith(Parameterized.class) +public class GitChangeSetTimestampTest { + + private final String normalizedTimestamp; + private final long millisecondsSinceEpoch; + + private final GitChangeSet changeSet; + + public GitChangeSetTimestampTest(String timestamp, String normalizedTimestamp, long millisecondsSinceEpoch) { + this.normalizedTimestamp = normalizedTimestamp == null ? timestamp : normalizedTimestamp; + this.millisecondsSinceEpoch = millisecondsSinceEpoch; + changeSet = genChangeSet(timestamp); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection createSampleChangeSets() { + Object[][] samples = { + /* git whatchanged dates from various time zones, months, & days */ + {"2015-10-06 19:29:47 +0300", null, 1444148987000L}, + {"2017-10-23 23:43:29 +0100", null, 1508798609000L}, + {"2017-09-21 17:35:24 -0400", null, 1506029724000L}, + {"2017-07-18 08:34:48 -0800", null, 1500395688000L}, + {"2007-12-19 01:59:25 +0000", null, 1198029565000L}, + {"2007-12-19 01:59:25 -0000", null, 1198029565000L}, + {"2017-01-13 16:20:12 -0500", null, 1484342412000L}, + {"2016-12-24 20:08:55 +0900", null, 1482577735000L}, + /* nearly ISO 8601 formatted dates from various time zones, months, & days */ + {"2013-03-21T15:16:44+0100", null, 1363875404000L}, + {"2014-11-13T01:42:14-0700", null, 1415868134000L}, + {"2010-06-24T20:08:27+0200", null, 1277402907000L}, + /* Seconds since epoch dates from various time zones, months, & days */ + {"1363879004 +0100", "2013-03-21T15:16:44+0100", 1363875404000L}, + {"1415842934 -0700", "2014-11-13T01:42:14-0700", 1415868134000L}, + {"1277410107 +0200", "2010-06-24T20:08:27+0200", 1277402907000L}, + {"1234567890 +0000", "2009-02-13T23:31:30+0000", 1234567890000L}, + /* ISO 8601 formatted dates from various time zones, months, & days */ + {"2013-03-21T15:16:44+01:00", null, 1363875404000L}, + {"2014-11-13T01:42:14-07:00", null, 1415868134000L}, + {"2010-06-24T20:08:27+02:00", null, 1277402907000L}, + /* Invalid date */ + {"2010-06-24 20:08:27am +02:00", null, -1L} + }; + List values = new ArrayList<>(samples.length); + values.addAll(Arrays.asList(samples)); + return values; + } + + @Test + public void testChangeSetDate() { + assertThat(changeSet.getDate(), is(normalizedTimestamp)); + } + + @Test + @Issue("JENKINS-30073") + public void testChangeSetTimeStamp() { + assertThat(changeSet.getTimestamp(), is(millisecondsSinceEpoch)); + } + + private final Random random = new Random(); + + private GitChangeSet genChangeSet(String timestamp) { + boolean authorOrCommitter = random.nextBoolean(); + String[] linesArray = { + "commit 302548f75c3eb6fa1db83634e4061d0ded416e5a", + "tree e1bd430d3f45b7aae54a3061b7895ee1858ec1f8", + "parent c74f084d8f9bc9e52f0b3fe9175ad27c39947a73", + "author Viacheslav Kopchenin " + timestamp, + "committer Viacheslav Kopchenin " + timestamp, + "", + " pom.xml", + " ", + " :100644 100644 bb32d78c69a7bf79849217bc02b1ba2c870a5a66 343a844ad90466d8e829896c1827ca7511d0d1ef M modules/platform/pom.xml", + "" + }; + ArrayList lines = new ArrayList<>(linesArray.length); + lines.addAll(Arrays.asList(linesArray)); + return new GitChangeSet(lines, authorOrCommitter); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetTruncateTest.java b/src/test/java/hudson/plugins/git/GitChangeSetTruncateTest.java new file mode 100644 index 0000000000..f2faab8034 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetTruncateTest.java @@ -0,0 +1,183 @@ +package hudson.plugins.git; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import jenkins.plugins.git.CliGitCommand; +import jenkins.plugins.git.GitSampleRepoRule; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; + +@RunWith(Parameterized.class) +public class GitChangeSetTruncateTest { + + @ClassRule + public static TemporaryFolder tempFolder = new TemporaryFolder(); + + @ClassRule + public static GitSampleRepoRule versionCheckRepo = new GitSampleRepoRule(); + + private static File repoRoot = null; + + private static final Random random = new Random(); + + /* Arguments to the constructor */ + private final String gitImpl; + private final String commitSummary; + private final String truncatedSummary; + + /* Computed in the constructor, used in tests */ + private final GitChangeSet changeSet; + private final GitChangeSet changeSetFullSummary; + private final GitChangeSet changeSetTruncatedSummary; + + private static class TestData { + + final public String testDataCommitSummary; + final public String testDataTruncatedSummary; + + TestData(String commitSummary, String truncatedSummary) { + this.testDataCommitSummary = commitSummary; + this.testDataTruncatedSummary = truncatedSummary; + } + } + + // 1 2 3 4 5 6 7 + // 1234567890123456789012345678901234567890123456789012345678901234567890 + private final static String SEVENTY_CHARS = "[JENKINS-012345] 8901 34567 90 23456 8901 34567 9012 4567890 2345678 0"; + private final static String EIGHTY_CHARS = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + private final static TestData[] TEST_DATA = { + new TestData(EIGHTY_CHARS, EIGHTY_CHARS), // surprising that longer than 72 is returned + new TestData(EIGHTY_CHARS + " A B C", EIGHTY_CHARS), // surprising that longer than 72 is returned + new TestData(SEVENTY_CHARS, SEVENTY_CHARS), + new TestData(SEVENTY_CHARS + " 2", SEVENTY_CHARS + " 2"), + new TestData(SEVENTY_CHARS + " 2 4", SEVENTY_CHARS + " 2"), + new TestData(SEVENTY_CHARS + " 23", SEVENTY_CHARS), + new TestData(SEVENTY_CHARS + " 2&4", SEVENTY_CHARS), + new TestData(SEVENTY_CHARS + "1", SEVENTY_CHARS + "1"), + new TestData(SEVENTY_CHARS + "1 3", SEVENTY_CHARS + "1"), + new TestData(SEVENTY_CHARS + "1 <4", SEVENTY_CHARS + "1"), + new TestData(SEVENTY_CHARS + "1 3 5", SEVENTY_CHARS + "1"), + new TestData(SEVENTY_CHARS + "1;", SEVENTY_CHARS + "1;"), + new TestData(SEVENTY_CHARS + "1; 4", SEVENTY_CHARS + "1;"), + new TestData(SEVENTY_CHARS + " " + SEVENTY_CHARS, SEVENTY_CHARS), + new TestData(SEVENTY_CHARS + " " + SEVENTY_CHARS, SEVENTY_CHARS + " ") // surprising that trailing space is preserved + }; + + public GitChangeSetTruncateTest(String gitImpl, String commitSummary, String truncatedSummary) throws Exception { + this.gitImpl = gitImpl; + this.commitSummary = commitSummary; + this.truncatedSummary = truncatedSummary; + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(repoRoot).using(gitImpl).getClient(); + final ObjectId head = commitOneFile(gitClient, commitSummary); + StringWriter changelogStringWriter = new StringWriter(); + gitClient.changelog().includes(head).to(changelogStringWriter).execute(); + List changeLogList = Arrays.asList(changelogStringWriter.toString().split("\n")); + changeSet = new GitChangeSet(changeLogList, random.nextBoolean()); + changeSetFullSummary = new GitChangeSet(changeLogList, random.nextBoolean(), true); + changeSetTruncatedSummary = new GitChangeSet(changeLogList, random.nextBoolean(), false); + } + + @Parameterized.Parameters(name = "{0} \"{1}\" --->>> \"{2}\"") + public static Collection gitObjects() { + /* If CLI git is older than 1.8.3, don't test CLI git message truncation */ + /* CLI git 1.7.1 (CentOS 6) does not support the message truncation command line flags */ + String[] bothGitImplementations = {"git", "jgit"}; + String[] jgitImplementation = {"jgit"}; + List arguments = new ArrayList<>(); + for (String implementation : versionCheckRepo.gitVersionAtLeast(1, 8, 3) ? bothGitImplementations : jgitImplementation) { + for (TestData sample : TEST_DATA) { + Object[] item = {implementation, sample.testDataCommitSummary, sample.testDataTruncatedSummary}; + arguments.add(item); + } + } + Collections.shuffle(arguments); // Execute in random order + return arguments; + } + + @BeforeClass + public static void createRepo() throws Exception { + repoRoot = tempFolder.newFolder(); + String initialImpl = random.nextBoolean() ? "git" : "jgit"; + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(repoRoot).using(initialImpl).getClient(); + gitClient.init_().workspace(repoRoot.getAbsolutePath()).execute(); + new CliGitCommand(gitClient, "config", "user.name", "ChangeSet Truncation Test"); + new CliGitCommand(gitClient, "config", "user.email", "ChangeSetTruncation@example.com"); + } + + private ObjectId commitOneFile(GitClient gitClient, final String commitSummary) throws Exception { + String path = "One-File.txt"; + String content = String.format("A random UUID: %s\n", UUID.randomUUID().toString()); + /* randomize whether commit message is single line or multi-line */ + String commitMessageBody = random.nextBoolean() ? "\n\n" + "committing " + path + " with content:\n\n" + content : ""; + String commitMessage = commitSummary + commitMessageBody; + createFile(path, content); + gitClient.add(path); + gitClient.commit(commitMessage); + List headList = gitClient.revList(Constants.HEAD); + assertThat(headList.size(), is(greaterThan(0))); + return headList.get(0); + } + + private void createFile(String path, String content) throws Exception { + File aFile = new File(repoRoot, path); + File parentDir = aFile.getParentFile(); + if (parentDir != null) { + parentDir.mkdirs(); + } + try (PrintWriter writer = new PrintWriter(aFile, "UTF-8")) { + writer.printf(content); + } catch (FileNotFoundException | UnsupportedEncodingException ex) { + throw new GitException(ex); + } + } + + @Test + @Issue("JENKINS-29977") // CLI git truncates first line of commit message in Changes page, JGit doesn't + public void summaryTruncatedAtLastWord72CharactersOrLess() throws Exception { + /** + * Before git plugin 4.0, calls to GitChangeSet(x, y) truncated CLI git, did not truncate JGit. + * After git plugin 4.0, calls to GitChangeSet(x, y) truncates CLI git, truncates JGit. + * Callers after git plugin 4.0 must use the GitChangeSet(x, y, z) call to specify truncation behavior. + */ + assertThat(changeSet.getMsg(), is(truncatedSummary)); + } + + @Test + @Issue("JENKINS-29977") + public void summaryAlwaysTruncatedAtLastWord72CharactersOrLess() throws Exception { + assertThat(changeSetTruncatedSummary.getMsg(), is(truncatedSummary)); + } + + @Test + @Issue("JENKINS-29977") + public void summaryNotTruncatedAtLastWord72CharactersOrLess() throws Exception { + assertThat(changeSetFullSummary.getMsg(), is(commitSummary)); + } +} diff --git a/src/test/java/hudson/plugins/git/GitChangeSetUtil.java b/src/test/java/hudson/plugins/git/GitChangeSetUtil.java new file mode 100644 index 0000000000..d498e8b278 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitChangeSetUtil.java @@ -0,0 +1,139 @@ +package hudson.plugins.git; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.TaskListener; +import hudson.scm.EditType; +import hudson.util.StreamTaskListener; + +import org.eclipse.jgit.lib.ObjectId; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import junit.framework.TestCase; + +/** Utility class to support GitChangeSet testing. */ +public class GitChangeSetUtil { + + static final String ID = "123abc456def"; + static final String PARENT = "345mno678pqr"; + static final String AUTHOR_NAME = "John Author"; + static final String AUTHOR_DATE = "1234568 -0600"; + static final String AUTHOR_DATE_FORMATTED = "1970-01-15T06:56:08-0600"; + static final String AUTHOR_EMAIL = "jauthor@nospam.com"; + static final String COMMITTER_NAME = "John Committer"; + static final String COMMITTER_DATE = "1234566 -0600"; + static final String COMMITTER_DATE_FORMATTED = "1970-01-15T06:56:06-0600"; + static final String COMMIT_TITLE = "Commit title."; + static final String COMMENT = COMMIT_TITLE + "\n"; + static final String COMMITTER_EMAIL = "jcommitter@nospam.com"; + + static GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat) { + return genChangeSet(authorOrCommitter, useLegacyFormat, true); + } + + public static GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat, boolean hasParent) { + return genChangeSet(authorOrCommitter, useLegacyFormat, hasParent, COMMIT_TITLE); + } + + public static GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat, boolean hasParent, String commitTitle) { + return genChangeSet(authorOrCommitter, useLegacyFormat, hasParent, commitTitle, false); + } + + public static GitChangeSet genChangeSet(boolean authorOrCommitter, boolean useLegacyFormat, boolean hasParent, String commitTitle, boolean truncate) { + ArrayList lines = new ArrayList<>(); + lines.add("Some header junk we should ignore..."); + lines.add("header line 2"); + lines.add("commit " + ID); + lines.add("tree 789ghi012jkl"); + if (hasParent) { + lines.add("parent " + PARENT); + } else { + lines.add("parent "); + } + lines.add("author " + AUTHOR_NAME + " <" + AUTHOR_EMAIL + "> " + AUTHOR_DATE); + lines.add("committer " + COMMITTER_NAME + " <" + COMMITTER_EMAIL + "> " + COMMITTER_DATE); + lines.add(""); + lines.add(" " + commitTitle); + lines.add(" Commit extended description."); + lines.add(""); + if (useLegacyFormat) { + lines.add("123abc456def"); + lines.add(" create mode 100644 some/file1"); + lines.add(" delete mode 100644 other/file2"); + } + lines.add(":000000 123456 0000000000000000000000000000000000000000 123abc456def789abc012def345abc678def901a A\tsrc/test/add.file"); + lines.add(":123456 000000 123abc456def789abc012def345abc678def901a 0000000000000000000000000000000000000000 D\tsrc/test/deleted.file"); + lines.add(":123456 789012 123abc456def789abc012def345abc678def901a bc234def567abc890def123abc456def789abc01 M\tsrc/test/modified.file"); + lines.add(":123456 789012 123abc456def789abc012def345abc678def901a bc234def567abc890def123abc456def789abc01 R012\tsrc/test/renamedFrom.file\tsrc/test/renamedTo.file"); + lines.add(":000000 123456 bc234def567abc890def123abc456def789abc01 123abc456def789abc012def345abc678def901a C100\tsrc/test/original.file\tsrc/test/copyOf.file"); + return new GitChangeSet(lines, authorOrCommitter, truncate); + } + + static void assertChangeSet(GitChangeSet changeSet) { + TestCase.assertEquals("123abc456def", changeSet.getId()); + TestCase.assertEquals("Commit title.", changeSet.getMsg()); + TestCase.assertEquals("Commit title.\nCommit extended description.\n", changeSet.getComment()); + TestCase.assertEquals("Commit title.\nCommit extended description.\n".replace("\n", "
        "), changeSet.getCommentAnnotated()); + HashSet expectedAffectedPaths = new HashSet<>(7); + expectedAffectedPaths.add("src/test/add.file"); + expectedAffectedPaths.add("src/test/deleted.file"); + expectedAffectedPaths.add("src/test/modified.file"); + expectedAffectedPaths.add("src/test/renamedFrom.file"); + expectedAffectedPaths.add("src/test/renamedTo.file"); + expectedAffectedPaths.add("src/test/copyOf.file"); + TestCase.assertEquals(expectedAffectedPaths, changeSet.getAffectedPaths()); + Collection actualPaths = changeSet.getPaths(); + TestCase.assertEquals(6, actualPaths.size()); + for (GitChangeSet.Path path : actualPaths) { + if (null != path.getPath()) switch (path.getPath()) { + case "src/test/add.file": + TestCase.assertEquals(EditType.ADD, path.getEditType()); + TestCase.assertNull(path.getSrc()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); + break; + case "src/test/deleted.file": + TestCase.assertEquals(EditType.DELETE, path.getEditType()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + TestCase.assertNull(path.getDst()); + break; + case "src/test/modified.file": + TestCase.assertEquals(EditType.EDIT, path.getEditType()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + TestCase.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/renamedFrom.file": + TestCase.assertEquals(EditType.DELETE, path.getEditType()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + TestCase.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/renamedTo.file": + TestCase.assertEquals(EditType.ADD, path.getEditType()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getSrc()); + TestCase.assertEquals("bc234def567abc890def123abc456def789abc01", path.getDst()); + break; + case "src/test/copyOf.file": + TestCase.assertEquals(EditType.ADD, path.getEditType()); + TestCase.assertEquals("bc234def567abc890def123abc456def789abc01", path.getSrc()); + TestCase.assertEquals("123abc456def789abc012def345abc678def901a", path.getDst()); + break; + default: + TestCase.fail("Unrecognized path."); + break; + } + } + } + + public static GitChangeSet genChangeSet(ObjectId sha1, String gitImplementation, boolean authorOrCommitter) throws IOException, InterruptedException { + EnvVars envVars = new EnvVars(); + TaskListener listener = StreamTaskListener.fromStdout(); + GitClient git = Git.with(listener, envVars).in(new FilePath(new File("."))).using(gitImplementation).getClient(); + return new GitChangeSet(git.showRevision(sha1), authorOrCommitter); + } +} diff --git a/src/test/java/hudson/plugins/git/GitPublisherTest.java b/src/test/java/hudson/plugins/git/GitPublisherTest.java index 717a0566c4..2e49dda39e 100644 --- a/src/test/java/hudson/plugins/git/GitPublisherTest.java +++ b/src/test/java/hudson/plugins/git/GitPublisherTest.java @@ -23,6 +23,7 @@ */ package hudson.plugins.git; +import hudson.FilePath; import hudson.Launcher; import hudson.matrix.Axis; import hudson.matrix.AxisList; @@ -35,38 +36,66 @@ import hudson.plugins.git.extensions.GitSCMExtension; import hudson.plugins.git.extensions.impl.LocalBranch; import hudson.plugins.git.extensions.impl.PreBuildMerge; -import hudson.plugins.git.UserMergeOptions; import hudson.scm.NullSCM; import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.util.StreamTaskListener; import org.eclipse.jgit.lib.Constants; -import org.jvnet.hudson.test.Bug; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.jvnet.hudson.test.Issue; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import jenkins.model.Jenkins; +import jenkins.plugins.git.CliGitCommand; +import org.eclipse.jgit.lib.PersonIdent; +import org.jenkinsci.plugins.gitclient.GitClient; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assume.assumeThat; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + /** * Tests for {@link GitPublisher} * * @author Kohsuke Kawaguchi */ -public class GitPublisherTest extends AbstractGitTestCase { - @Bug(5005) +public class GitPublisherTest extends AbstractGitProject { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @BeforeClass + public static void setGitDefaults() throws Exception { + CliGitCommand gitCmd = new CliGitCommand(null); + gitCmd.setDefaults(); + } + + @Issue("JENKINS-5005") + @Test public void testMatrixBuild() throws Exception { final AtomicInteger run = new AtomicInteger(); // count the number of times the perform is called - commit("a", johnDoe, "commit #1"); + commitNewFile("a"); - MatrixProject mp = createMatrixProject("xyz"); + MatrixProject mp = jenkins.createProject(MatrixProject.class, "xyz"); mp.setAxes(new AxisList(new Axis("VAR","a","b"))); - mp.setScm(new GitSCM(workDir.getAbsolutePath())); + mp.setScm(new GitSCM(testGitDir.getAbsolutePath())); mp.getPublishersList().add(new GitPublisher( Collections.singletonList(new TagToPush("origin","foo","message",true, false)), Collections.emptyList(), Collections.emptyList(), - true, true) { + true, true, false) { @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { run.incrementAndGet(); @@ -81,14 +110,13 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen @Override public BuildStepDescriptor getDescriptor() { - return (BuildStepDescriptor)Hudson.getInstance().getDescriptorOrDie(GitPublisher.class); // fake + return (BuildStepDescriptor)Jenkins.get().getDescriptorOrDie(GitPublisher.class); // fake } private Object writeReplace() { return new NullSCM(); } }); - MatrixBuild b = assertBuildStatusSuccess(mp.scheduleBuild2(0).get()); - System.out.println(b.getLog()); + MatrixBuild b = jenkins.assertBuildStatusSuccess(mp.scheduleBuild2(0).get()); assertTrue(existsTag("foo")); @@ -98,16 +126,17 @@ public BuildStepDescriptor getDescriptor() { assertEquals(3,run.get()); } + @Test public void testMergeAndPush() throws Exception { FreeStyleProject project = setupSimpleProject("master"); GitSCM scm = new GitSCM( - createRemoteRepositories(), + remoteConfigs(), Collections.singletonList(new BranchSpec("*")), false, Collections.emptyList(), null, null, Collections.emptyList()); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, null))); scm.getExtensions().add(new LocalBranch("integration")); project.setScm(scm); @@ -115,37 +144,440 @@ public void testMergeAndPush() throws Exception { Collections.emptyList(), Collections.singletonList(new BranchToPush("origin", "integration")), Collections.emptyList(), - true, true)); + true, true, false)); // create initial commit and then run the build against it: - commit("commitFileBase", johnDoe, "Initial Commit"); - testRepo.git.branch("integration"); + commitNewFile("commitFileBase"); + testGitClient.branch("integration"); build(project, Result.SUCCESS, "commitFileBase"); - testRepo.git.checkout(null, "topic1"); + testGitClient.checkout(null, "topic1"); final String commitFile1 = "commitFile1"; - commit(commitFile1, johnDoe, "Commit number 1"); + commitNewFile(commitFile1); final FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); assertTrue(build1.getWorkspace().child(commitFile1).exists()); String sha1 = getHeadRevision(build1, "integration"); - assertEquals(sha1, testRepo.git.revParse(Constants.HEAD).name()); + assertEquals(sha1, testGitClient.revParse(Constants.HEAD).name()); + + } + + @Issue("JENKINS-12402") + @Test + public void testMergeAndPushFF() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, MergeCommand.GitPluginFastForwardMode.FF))); + scm.getExtensions().add(new LocalBranch("integration")); + project.setScm(scm); + + project.getPublishersList().add(new GitPublisher( + Collections.emptyList(), + Collections.singletonList(new BranchToPush("origin", "integration")), + Collections.emptyList(), + true, true, false)); + + // create initial commit and then run the build against it: + commitNewFile("commitFileBase"); + testGitClient.branch("integration"); + final FreeStyleBuild build1 = build(project, Result.SUCCESS, "commitFileBase"); + assertTrue(build1.getWorkspace().child("commitFileBase").exists()); + String shaIntegration = getHeadRevision(build1, "integration"); + assertEquals("the integration branch should be at HEAD", shaIntegration, testGitClient.revParse(Constants.HEAD).name()); + + // create a new branch and build, this results in a fast-forward merge + testGitClient.checkout("master"); + ObjectId master = testGitClient.revParse("HEAD"); + testGitClient.branch("branch1"); + testGitClient.checkout("branch1"); + final String commitFile1 = "commitFile1"; + commitNewFile(commitFile1); + String shaBranch1 = testGitClient.revParse("branch1").name(); + final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile1); + // Test that the build (including publish) performed as expected. + // - commitFile1 is in the workspace + // - HEAD and integration should line up with branch1 like so: + // * f4d190c (HEAD, integration, branch1) Commit number 1 + // * f787536 (master) Initial Commit + // + assertTrue(build2.getWorkspace().child("commitFile1").exists()); + shaIntegration = getHeadRevision(build2, "integration"); + String shaHead = testGitClient.revParse(Constants.HEAD).name(); + assertEquals("the integration branch and branch1 should line up",shaIntegration, shaBranch1); + assertEquals("the integration branch should be at HEAD",shaIntegration, shaHead); + // integration should have master as the parent commit + List revList = testGitClient.revList("integration^1"); + ObjectId integrationParent = revList.get(0); + assertEquals("Fast-forward merge should have had master as a parent",master,integrationParent); + + // create a second branch off of master, so as to force a merge commit and to test + // that --ff gracefully falls back to a merge commit + testGitClient.checkout("master"); + testGitClient.branch("branch2"); + testGitClient.checkout("branch2"); + final String commitFile2 = "commitFile2"; + commitNewFile(commitFile2); + String shaBranch2 = testGitClient.revParse("branch2").name(); + final FreeStyleBuild build3 = build(project, Result.SUCCESS, commitFile2); + + // Test that the build (including publish) performed as expected + // - commitFile1 is in the workspace + // - commitFile2 is in the workspace + // - the integration branch has branch1 and branch2 as parents, like so: + // * f9b37d8 (integration) Merge commit '96a11fd...' into integration + // |\ + // | * 96a11fd (HEAD, branch2) Commit number 2 + // * | f4d190c (branch1) Commit number 1 + // |/ + // * f787536 (master) Initial Commit + // + assertTrue(build1.getWorkspace().child(commitFile1).exists()); + assertTrue(build1.getWorkspace().child(commitFile2).exists()); + // the integration branch should have branch1 and branch2 as parents + revList = testGitClient.revList("integration^1"); + assertEquals("Integration should have branch1 as a parent",revList.get(0).name(),shaBranch1); + revList = testGitClient.revList("integration^2"); + assertEquals("Integration should have branch2 as a parent",revList.get(0).name(),shaBranch2); } - /** - * Fix push to remote when skipTag is enabled - */ - @Bug(17769) + @Issue("JENKINS-12402") + @Test + public void testMergeAndPushNoFF() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, MergeCommand.GitPluginFastForwardMode.NO_FF))); + scm.getExtensions().add(new LocalBranch("integration")); + project.setScm(scm); + + project.getPublishersList().add(new GitPublisher( + Collections.emptyList(), + Collections.singletonList(new BranchToPush("origin", "integration")), + Collections.emptyList(), + true, true, false)); + + // create initial commit and then run the build against it: + commitNewFile("commitFileBase"); + testGitClient.branch("integration"); + final FreeStyleBuild build1 = build(project, Result.SUCCESS, "commitFileBase"); + assertTrue(build1.getWorkspace().child("commitFileBase").exists()); + String shaIntegration = getHeadRevision(build1, "integration"); + assertEquals("integration branch should be at HEAD", shaIntegration, testGitClient.revParse(Constants.HEAD).name()); + + // create a new branch and build + // This would be a fast-forward merge, but we're calling for --no-ff and that should work + testGitClient.checkout("master"); + ObjectId master = testGitClient.revParse("HEAD"); + testGitClient.branch("branch1"); + testGitClient.checkout("branch1"); + final String commitFile1 = "commitFile1"; + commitNewFile(commitFile1); + String shaBranch1 = testGitClient.revParse("branch1").name(); + final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile1); + ObjectId mergeCommit = testGitClient.revParse("integration"); + + // Test that the build and publish performed as expected. + // - commitFile1 is in the workspace + // - integration has branch1 and master as parents, like so: + // * 6913e57 (integration) Merge commit '257e33c...' into integration + // |\ + // | * 257e33c (HEAD, branch1) Commit number 1 + // |/ + // * 3066c87 (master) Initial Commit + // + assertTrue(build2.getWorkspace().child("commitFile1").exists()); + List revList = testGitClient.revList("integration^1"); + assertEquals("Integration should have master as a parent",revList.get(0),master); + revList = testGitClient.revList("integration^2"); + assertEquals("Integration should have branch1 as a parent",revList.get(0).name(),shaBranch1); + + // create a second branch off of master, so as to test that --no-ff is published as expected + testGitClient.checkout("master"); + testGitClient.branch("branch2"); + testGitClient.checkout("branch2"); + final String commitFile2 = "commitFile2"; + commitNewFile(commitFile2); + String shaBranch2 = testGitClient.revParse("branch2").name(); + final FreeStyleBuild build3 = build(project, Result.SUCCESS, commitFile2); + + // Test that the build performed as expected + // - commitFile1 is in the workspace + // - commitFile2 is in the workspace + // - the integration branch has branch1 and the previous merge commit as parents, like so: + // * 5908447 (integration) Merge commit '157fd0b...' into integration + // |\ + // | * 157fd0b (HEAD, branch2) Commit number 2 + // * | 7afa661 Merge commit '0a37dd6...' into integration + // |\ \ + // | |/ + // |/| + // | * 0a37dd6 (branch1) Commit number 1 + // |/ + // * a5dda1a (master) Initial Commit + // + assertTrue("commitFile1 should exist in the workspace",build1.getWorkspace().child(commitFile1).exists()); + assertTrue("commitFile2 should exist in the workspace",build1.getWorkspace().child(commitFile2).exists()); + // the integration branch should have branch1 and branch2 as parents + revList = testGitClient.revList("integration^1"); + assertEquals("Integration should have the first merge commit as a parent",revList.get(0),mergeCommit); + revList = testGitClient.revList("integration^2"); + assertEquals("Integration should have branch2 as a parent",revList.get(0).name(),shaBranch2); + } + + @Issue("JENKINS-12402") + @Test + public void testMergeAndPushFFOnly() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, MergeCommand.GitPluginFastForwardMode.FF_ONLY))); + scm.getExtensions().add(new LocalBranch("integration")); + project.setScm(scm); + + project.getPublishersList().add(new GitPublisher( + Collections.emptyList(), + Collections.singletonList(new BranchToPush("origin", "integration")), + Collections.emptyList(), + true, true, false)); + + // create initial commit and then run the build against it: + commitNewFile("commitFileBase"); + testGitClient.branch("integration"); + final FreeStyleBuild build1 = build(project, Result.SUCCESS, "commitFileBase"); + assertTrue(build1.getWorkspace().child("commitFileBase").exists()); + String shaIntegration = getHeadRevision(build1, "integration"); + assertEquals("integration should be at HEAD", shaIntegration, testGitClient.revParse(Constants.HEAD).name()); + + // create a new branch and build + // This merge can work with --ff-only + testGitClient.checkout("master"); + ObjectId master = testGitClient.revParse("HEAD"); + testGitClient.branch("branch1"); + testGitClient.checkout("branch1"); + final String commitFile1 = "commitFile1"; + commitNewFile(commitFile1); + String shaBranch1 = testGitClient.revParse("branch1").name(); + final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile1); + ObjectId mergeCommit = testGitClient.revParse("integration"); + + // Test that the build (including publish) performed as expected. + // - commitFile1 is in the workspace + // - HEAD and integration should line up with branch1 like so: + // * f4d190c (HEAD, integration, branch1) Commit number 1 + // * f787536 (master) Initial Commit + // + assertTrue("commitFile1 should exist in the workspace",build2.getWorkspace().child("commitFile1").exists()); + shaIntegration = getHeadRevision(build2, "integration"); + String shaHead = testGitClient.revParse(Constants.HEAD).name(); + assertEquals("integration and branch1 should line up",shaIntegration, shaBranch1); + assertEquals("integration and head should line up",shaIntegration, shaHead); + // integration should have master as the parent commit + List revList = testGitClient.revList("integration^1"); + ObjectId integrationParent = revList.get(0); + assertEquals("Fast-forward merge should have had master as a parent",master,integrationParent); + + // create a second branch off of master, so as to force a merge commit + // but the publish will fail as --ff-only cannot work with a parallel branch + testGitClient.checkout("master"); + testGitClient.branch("branch2"); + testGitClient.checkout("branch2"); + final String commitFile2 = "commitFile2"; + commitNewFile(commitFile2); + String shaBranch2 = testGitClient.revParse("branch2").name(); + final FreeStyleBuild build3 = build(project, Result.FAILURE, commitFile2); + + // Test that the publish did not merge the branches + // - The workspace will contain commitFile2, but not branch1's file (commitFile1) + // - The repository will be left with branch2 unmerged like so: + // * c19a55d (HEAD, branch2) Commit number 2 + // | * 79c49b2 (integration, branch1) Commit number 1 + // |/ + // * ebffeb3 (master) Initial Commit + assertFalse("commitFile1 should not exist in the workspace",build2.getWorkspace().child("commitFile1").exists()); + assertTrue("commitFile2 should exist in the workspace",build2.getWorkspace().child("commitFile2").exists()); + revList = testGitClient.revList("branch2^1"); + assertEquals("branch2 should have master as a parent",revList.get(0),master); + try { + revList = testGitClient.revList("branch2^2"); + assertTrue("branch2 should have no other parent than master",false); + } catch (java.lang.NullPointerException err) { + // expected + } + } + + @Issue("JENKINS-24786") + @Test + public void testPushEnvVarsInRemoteConfig() throws Exception{ + FreeStyleProject project = setupSimpleProject("master"); + + // create second (bare) test repository as target + TaskListener listener = StreamTaskListener.fromStderr(); + TestGitRepo testTargetRepo = new TestGitRepo("target", tmpFolder.newFolder("push_env_vars"), listener); + testTargetRepo.git.init_().workspace(testTargetRepo.gitDir.getAbsolutePath()).bare(true).execute(); + testTargetRepo.commit("lostTargetFile", new PersonIdent("John Doe", "john@example.com"), "Initial Target Commit"); + + // add second test repository as remote repository with environment variables + List remoteRepositories = remoteConfigs(); + remoteRepositories.add(new UserRemoteConfig("$TARGET_URL", "$TARGET_NAME", "+refs/heads/$TARGET_BRANCH:refs/remotes/$TARGET_NAME/$TARGET_BRANCH", null)); + + GitSCM scm = new GitSCM( + remoteRepositories, + Collections.singletonList(new BranchSpec("origin/master")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + + // add parameters for remote repository configuration + project.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("TARGET_URL", testTargetRepo.gitDir.getAbsolutePath()), + new StringParameterDefinition("TARGET_NAME", "target"), + new StringParameterDefinition("TARGET_BRANCH", "master"))); + + String tag_name = "test-tag"; + String note_content = "Test Note"; + + project.getPublishersList().add(new GitPublisher( + Collections.singletonList(new TagToPush("$TARGET_NAME", tag_name, "", false, false)), + Collections.singletonList(new BranchToPush("$TARGET_NAME", "$TARGET_BRANCH")), + Collections.singletonList(new NoteToPush("$TARGET_NAME", note_content, Constants.R_NOTES_COMMITS, false)), + true, false, true)); + + commitNewFile("commitFile"); + testGitClient.tag(tag_name, "Comment"); + ObjectId expectedCommit = testGitClient.revParse("master"); + + build(project, Result.SUCCESS, "commitFile"); + + // check if everything reached target repository + assertEquals(expectedCommit, testTargetRepo.git.revParse("master")); + assertTrue(existsTagInRepo(testTargetRepo.git, tag_name)); + + } + + @Issue("JENKINS-24082") + @Test + public void testForcePush() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("master")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + + GitPublisher forcedPublisher = new GitPublisher( + Collections.emptyList(), + Collections.singletonList(new BranchToPush("origin", "otherbranch")), + Collections.emptyList(), + true, true, true); + project.getPublishersList().add(forcedPublisher); + + // Create a commit on the master branch in the test repo + commitNewFile("commitFile"); + ObjectId masterCommit1 = testGitClient.revParse("master"); + + // Checkout and commit to "otherbranch" in the test repo + testGitClient.branch("otherbranch"); + testGitClient.checkout("otherbranch"); + commitNewFile("otherCommitFile"); + ObjectId otherCommit = testGitClient.revParse("otherbranch"); + + testGitClient.checkout("master"); + commitNewFile("commitFile2"); + ObjectId masterCommit2 = testGitClient.revParse("master"); + + // masterCommit1 parent of both masterCommit2 and otherCommit + assertEquals(masterCommit1, testGitClient.revParse("master^")); + assertEquals(masterCommit1, testGitClient.revParse("otherbranch^")); + + // Confirm that otherbranch still points to otherCommit + // build will merge and push to "otherbranch" in test repo + // Without force, this would fail + assertEquals(otherCommit, testGitClient.revParse("otherbranch")); // not merged yet + assertTrue("otherCommit not in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit)); + build(project, Result.SUCCESS, "commitFile2"); + assertEquals(masterCommit2, testGitClient.revParse("otherbranch")); // merge done + assertFalse("otherCommit in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit)); + + // Commit to otherbranch in test repo so that next merge will fail + testGitClient.checkout("otherbranch"); + commitNewFile("otherCommitFile2"); + ObjectId otherCommit2 = testGitClient.revParse("otherbranch"); + assertNotEquals(masterCommit2, otherCommit2); + + // Commit to master branch in test repo + testGitClient.checkout("master"); + commitNewFile("commitFile3"); + ObjectId masterCommit3 = testGitClient.revParse("master"); + + // Remove forcedPublisher, add unforcedPublisher + project.getPublishersList().remove(forcedPublisher); + GitPublisher unforcedPublisher = new GitPublisher( + Collections.emptyList(), + Collections.singletonList(new BranchToPush("origin", "otherbranch")), + Collections.emptyList(), + true, true, false); + project.getPublishersList().add(unforcedPublisher); + + // build will attempts to merge and push to "otherbranch" in test repo. + // Without force, will fail + assertEquals(otherCommit2, testGitClient.revParse("otherbranch")); // not merged yet + assertTrue("otherCommit2 not in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit2)); + build(project, Result.FAILURE, "commitFile3"); + assertEquals(otherCommit2, testGitClient.revParse("otherbranch")); // still not merged + assertTrue("otherCommit2 not in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit2)); + + // Remove unforcedPublisher, add forcedPublisher + project.getPublishersList().remove(unforcedPublisher); + project.getPublishersList().add(forcedPublisher); + + // Commit to master branch in test repo + testGitClient.checkout("master"); + commitNewFile("commitFile4"); + ObjectId masterCommit4 = testGitClient.revParse("master"); + + // build will merge and push to "otherbranch" in test repo. + assertEquals(otherCommit2, testGitClient.revParse("otherbranch")); + assertTrue("otherCommit2 not in test repo", testGitClient.isCommitInRepo(otherCommit2)); + assertTrue("otherCommit2 not in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit2)); + build(project, Result.SUCCESS, "commitFile4"); + assertEquals(masterCommit4, testGitClient.revParse("otherbranch")); + assertEquals(masterCommit3, testGitClient.revParse("otherbranch^")); + assertFalse("otherCommit2 in otherbranch", testGitClient.revList("otherbranch").contains(otherCommit2)); + } + + /* Fix push to remote when skipTag is enabled */ + @Issue("JENKINS-17769") + @Test public void testMergeAndPushWithSkipTagEnabled() throws Exception { FreeStyleProject project = setupSimpleProject("master"); GitSCM scm = new GitSCM( - createRemoteRepositories(), + remoteConfigs(), Collections.singletonList(new BranchSpec("*")), false, Collections.emptyList(), - null, null, new ArrayList()); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null))); + null, null, new ArrayList<>()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, null))); scm.getExtensions().add(new LocalBranch("integration")); project.setScm(scm); @@ -154,33 +586,227 @@ public void testMergeAndPushWithSkipTagEnabled() throws Exception { Collections.emptyList(), Collections.singletonList(new BranchToPush("origin", "integration")), Collections.emptyList(), - true, true)); + true, true, false)); // create initial commit and then run the build against it: - commit("commitFileBase", johnDoe, "Initial Commit"); - testRepo.git.branch("integration"); + commitNewFile("commitFileBase"); + testGitClient.branch("integration"); build(project, Result.SUCCESS, "commitFileBase"); - testRepo.git.checkout(null, "topic1"); + testGitClient.checkout(null, "topic1"); final String commitFile1 = "commitFile1"; - commit(commitFile1, johnDoe, "Commit number 1"); + commitNewFile(commitFile1); final FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); assertTrue(build1.getWorkspace().child(commitFile1).exists()); String sha1 = getHeadRevision(build1, "integration"); - assertEquals(sha1, testRepo.git.revParse(Constants.HEAD).name()); + assertEquals(sha1, testGitClient.revParse(Constants.HEAD).name()); + } + + @Test + public void testRebaseBeforePush() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("master")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + + BranchToPush btp = new BranchToPush("origin", "master"); + btp.setRebaseBeforePush(true); + + GitPublisher rebasedPublisher = new GitPublisher( + Collections.emptyList(), + Collections.singletonList(btp), + Collections.emptyList(), + true, true, true); + project.getPublishersList().add(rebasedPublisher); + + project.getBuildersList().add(new LongRunningCommit(testGitDir)); + project.save(); + + // Assume during our build someone else pushed changes (commitFile1) to the remote repo. + // So our own changes (commitFile2) cannot be pushed back to the remote origin. + // + // * 0eb2599 (HEAD) Added a file named commitFile2 + // | * 64e71e7 (origin/master) Added a file named commitFile1 + // |/ + // * b2578eb init + // + // What we can do is to fetch the remote changes and rebase our own changes: + // + // * 0e7674c (HEAD) Added a file named commitFile2 + // * 64e71e7 (origin/master) Added a file named commitFile1 + // * b2578eb init + + + // as we have set "rebaseBeforePush" to true we expect all files to be present after the build. + FreeStyleBuild build = build(project, Result.SUCCESS, "commitFile1", "commitFile2"); + } + + @Issue("JENKINS-24786") + @Test + public void testMergeAndPushWithCharacteristicEnvVar() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + /* + * JOB_NAME seemed like the more obvious choice, but when run from a + * multi-configuration job, the value of JOB_NAME includes an equals + * sign. That makes log parsing and general use of the variable more + * difficult. JENKINS_SERVER_COOKIE is a characteristic env var which + * probably never includes an equals sign. + */ + String envName = "JENKINS_SERVER_COOKIE"; + String envValue = project.getCharacteristicEnvVars().get(envName, "NOT-SET"); + assertFalse("Env " + envName + " not set", envValue.equals("NOT-SET")); + + checkEnvVar(project, envName, envValue); + } + + @Issue("JENKINS-24786") + @Test + public void testMergeAndPushWithSystemEnvVar() throws Exception { + String envName = isWindows() ? "COMPUTERNAME" : "LOGNAME"; + String envValue = System.getenv().get(envName); + assumeThat(envValue, notNullValue()); + assumeThat(envValue, not(isEmptyString())); + + FreeStyleProject project = setupSimpleProject("master"); + + assertNotNull("Env " + envName + " not set", envValue); + assertFalse("Env " + envName + " empty", envValue.isEmpty()); + + checkEnvVar(project, envName, envValue); + } + + private void checkEnvVar(FreeStyleProject project, String envName, String envValue) throws Exception { + + String envReference = "${" + envName + "}"; + + List scmExtensions = new ArrayList<>(); + scmExtensions.add(new PreBuildMerge(new UserMergeOptions("origin", envReference, null, null))); + scmExtensions.add(new LocalBranch(envReference)); + GitSCM scm = new GitSCM( + remoteConfigs(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, scmExtensions); + project.setScm(scm); + + String tagNameReference = envReference + "-tag"; // ${BRANCH_NAME}-tag + String tagNameValue = envValue + "-tag"; // master-tag + String tagMessageReference = envReference + " tag message"; + String noteReference = "note for " + envReference; + String noteValue = "note for " + envValue; + GitPublisher publisher = new GitPublisher( + Collections.singletonList(new TagToPush("origin", tagNameReference, tagMessageReference, false, true)), + Collections.singletonList(new BranchToPush("origin", envReference)), + Collections.singletonList(new NoteToPush("origin", noteReference, Constants.R_NOTES_COMMITS, false)), + true, true, true); + assertTrue(publisher.isForcePush()); + assertTrue(publisher.isPushBranches()); + assertTrue(publisher.isPushMerge()); + assertTrue(publisher.isPushNotes()); + assertTrue(publisher.isPushOnlyIfSuccess()); + assertTrue(publisher.isPushTags()); + project.getPublishersList().add(publisher); + + // create initial commit + commitNewFile("commitFileBase"); + ObjectId initialCommit = testGitClient.getHeadRev(testGitDir.getAbsolutePath(), "master"); + assertTrue(testGitClient.isCommitInRepo(initialCommit)); + + // Create branch in the test repo (pulled into the project workspace at build) + assertFalse("Test repo has " + envValue + " branch", hasBranch(envValue)); + testGitClient.branch(envValue); + assertTrue("Test repo missing " + envValue + " branch", hasBranch(envValue)); + assertFalse(tagNameValue + " in " + testGitClient, testGitClient.tagExists(tagNameValue)); + + // Build the branch + final FreeStyleBuild build0 = build(project, Result.SUCCESS, "commitFileBase"); + + String build0HeadBranch = getHeadRevision(build0, envValue); + assertEquals(build0HeadBranch, initialCommit.getName()); + assertTrue(tagNameValue + " not in " + testGitClient, testGitClient.tagExists(tagNameValue)); + assertTrue(tagNameValue + " not in build", build0.getWorkspace().child(".git/refs/tags/" + tagNameValue).exists()); + + // Create a topic branch in the source repository and commit to topic branch + String topicBranch = envValue + "-topic1"; + assertFalse("Test repo has " + topicBranch + " branch", hasBranch(topicBranch)); + testGitClient.checkout(null, topicBranch); + assertTrue("Test repo has no " + topicBranch + " branch", hasBranch(topicBranch)); + final String commitFile1 = "commitFile1"; + commitNewFile(commitFile1); + ObjectId topicCommit = testGitClient.getHeadRev(testGitDir.getAbsolutePath(), topicBranch); + assertTrue(testGitClient.isCommitInRepo(topicCommit)); + + // Run a build, should be on the topic branch, tagged, and noted + final FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + FilePath myWorkspace = build1.getWorkspace(); + assertTrue(myWorkspace.child(commitFile1).exists()); + assertTrue("Tag " + tagNameValue + " not in build", myWorkspace.child(".git/refs/tags/" + tagNameValue).exists()); + + String build1Head = getHeadRevision(build1, envValue); + assertEquals(build1Head, testGitClient.revParse(Constants.HEAD).name()); + assertEquals("Wrong head commit in build1", topicCommit.getName(), build1Head); } private boolean existsTag(String tag) throws InterruptedException { - Set tags = git.getTagNames("*"); - System.out.println(tags); + return existsTagInRepo(testGitClient, tag); + } + + private boolean existsTagInRepo(GitClient gitClient, String tag) throws InterruptedException { + Set tags = gitClient.getTagNames("*"); return tags.contains(tag); } private boolean containsTagMessage(String tag, String str) throws InterruptedException { - String msg = git.getTagMessage(tag); - System.out.println(msg); + String msg = testGitClient.getTagMessage(tag); return msg.contains(str); } + + private boolean hasBranch(String branchName) throws GitException, InterruptedException { + Set testRepoBranches = testGitClient.getBranches(); + for (Branch branch : testRepoBranches) { + if (branch.getName().equals(branchName)) { + return true; + } + } + return false; + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return java.io.File.pathSeparatorChar==';'; + } } + +class LongRunningCommit extends Builder { + + private File remoteGitDir; + + LongRunningCommit(File remoteGitDir) { + this.remoteGitDir = remoteGitDir; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + + TestGitRepo workspaceGit = new TestGitRepo("workspace", new File(build.getWorkspace().getRemote()), listener); + TestGitRepo remoteGit = new TestGitRepo("remote", this.remoteGitDir, listener); + + // simulate an external commit and push to the remote during the build of our project. + ObjectId headRev = remoteGit.git.revParse("HEAD"); + remoteGit.commit("commitFile1", remoteGit.johnDoe, "Added a file commitFile1"); + remoteGit.git.checkout(headRev.getName()); // allow to push to this repo later + + // commit onto the initial commit (creates a head with our changes later). + workspaceGit.commit("commitFile2", remoteGit.johnDoe, "Added a file commitFile2"); + + return true; + } +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/git/GitRevisionTokenMacroTest.java b/src/test/java/hudson/plugins/git/GitRevisionTokenMacroTest.java new file mode 100644 index 0000000000..f201e70c5c --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitRevisionTokenMacroTest.java @@ -0,0 +1,104 @@ +/* + * The MIT License + * + * Copyright 2019 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.model.AbstractBuild; +import hudson.model.TaskListener; +import hudson.plugins.git.util.BuildData; +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; +import org.junit.Before; +import org.mockito.Mockito; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class GitRevisionTokenMacroTest { + + private GitRevisionTokenMacro tokenMacro; + + public GitRevisionTokenMacroTest() { + } + + @Before + public void createTokenMacro() { + tokenMacro = new GitRevisionTokenMacro(); + } + + @Test + public void testAcceptsMacroName() { + assertTrue(tokenMacro.acceptsMacroName("GIT_REVISION")); + } + + @Test + public void testAcceptsMacroNameFalse() { + assertFalse(tokenMacro.acceptsMacroName("NOT_A_GIT_REVISION")); + } + + @Test(expected = NullPointerException.class) + public void testEvaluate() throws Exception { + // Real test in GitSCMTest#testBasicRemotePoll + tokenMacro.evaluate(null, TaskListener.NULL, "GIT_REVISION"); + } + + @Test + public void testEvaluateMockBuildNull() throws Exception { + // Real test in GitSCMTest#testBasicRemotePoll + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getAction(BuildData.class)).thenReturn(null); + assertThat(tokenMacro.evaluate(build, TaskListener.NULL, "GIT_REVISION"), is("")); + } + + @Test + public void testEvaluateMockBuildDataNull() throws Exception { + // Real test in GitSCMTest#testBasicRemotePoll + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(null); + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getAction(BuildData.class)).thenReturn(buildData); + assertThat(tokenMacro.evaluate(build, TaskListener.NULL, "GIT_REVISION"), is("")); + } + + @Test + public void testEvaluateMockBuildData() throws Exception { + // Real test in GitSCMTest#testBasicRemotePoll + Revision revision = new Revision(ObjectId.fromString("42ab63c2d69c012122d9b373450404244cc58e81")); + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getAction(BuildData.class)).thenReturn(buildData); + assertThat(tokenMacro.evaluate(build, TaskListener.NULL, "GIT_REVISION"), is(revision.getSha1String())); + } + + @Test + public void testEvaluateMockBuildDataLength() throws Exception { + // Real test in GitSCMTest#testBasicRemotePoll + Revision revision = new Revision(ObjectId.fromString("42ab63c2d69c012122d9b373450404244cc58e81")); + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getAction(BuildData.class)).thenReturn(buildData); + tokenMacro.length = 8; + assertThat(tokenMacro.evaluate(build, TaskListener.NULL, "GIT_REVISION"), is(revision.getSha1String().substring(0, 8))); + } +} diff --git a/src/test/java/hudson/plugins/git/GitSCMBrowserTest.java b/src/test/java/hudson/plugins/git/GitSCMBrowserTest.java new file mode 100644 index 0000000000..044c55b2fe --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitSCMBrowserTest.java @@ -0,0 +1,139 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.browser.GitLab; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.browser.GithubWeb; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitSCMBrowserTest { + + private final String gitURI; + private final Class expectedClass; + private final String expectedURI; + + public GitSCMBrowserTest(String gitURI, + Class expectedClass, + String expectedURI) { + this.gitURI = gitURI; + this.expectedClass = expectedClass; + this.expectedURI = expectedURI; + } + + private static Class expectedClass(String url) { + if (url.contains("bitbucket.org")) { + return BitbucketWeb.class; + } + if (url.contains("gitlab.com")) { + return GitLab.class; + } + if (url.contains("github.com")) { + return GithubWeb.class; + } + return null; + } + + private static final String REPO_PATH = "jenkinsci/git-plugin"; + + private static String expectedURL(String url) { + if (url.contains("bitbucket.org")) { + return "https://bitbucket.org/" + REPO_PATH + "/"; + } + if (url.contains("gitlab.com")) { + return "https://gitlab.com/" + REPO_PATH + "/"; + } + if (url.contains("github.com")) { + return "https://github.com/" + REPO_PATH + "/"; + } + return null; + + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteRepositoryURL() { + /* Systematically formed test URLs */ + String[] protocols = {"https", "ssh", "git"}; + String[] usernames = {"git:password@", "git@", "bob@", ""}; + String[] hostnames = {"github.com", "bitbucket.org", "gitlab.com", "example.com"}; + String[] suffixes = {".git/", ".git", "/", ""}; + String[] slashes = {"//", "/", ""}; + List values = new ArrayList<>(); + for (String protocol : protocols) { + for (String username : usernames) { + for (String hostname : hostnames) { + for (String suffix : suffixes) { + String url = protocol + "://" + username + hostname + "/" + REPO_PATH + suffix; + Object[] testCase = {url, expectedClass(url), expectedURL(url)}; + values.add(testCase); + } + } + } + } + /* Secure shell URL with embedded port number */ + String protocol = "ssh"; + for (String username : usernames) { + for (String hostname : hostnames) { + for (String suffix : suffixes) { + String url = protocol + "://" + username + hostname + ":22/" + REPO_PATH + suffix; + Object[] testCase = {url, expectedClass(url), expectedURL(url)}; + values.add(testCase); + } + } + } + /* ssh alternate syntax */ + for (String hostname : hostnames) { + for (String suffix : suffixes) { + for (String slash : slashes) { + String url = "git@" + hostname + ":" + slash + REPO_PATH + suffix; + Object[] testCase = {url, expectedClass(url), expectedURL(url)}; + values.add(testCase); + } + } + } + return values; + } + + @Test + public void guessedBrowser() { + GitSCM gitSCM = new GitSCM(gitURI); + GitRepositoryBrowser browser = (GitRepositoryBrowser) gitSCM.guessBrowser(); + if (expectedClass == null || expectedURI == null) { + assertThat(browser, is(nullValue())); + } else { + assertThat(browser, is(instanceOf(expectedClass))); + assertThat(browser.getRepoUrl(), is(expectedURI)); + } + } +} diff --git a/src/test/java/hudson/plugins/git/GitSCMTest.java b/src/test/java/hudson/plugins/git/GitSCMTest.java index eba1466825..df5e9c16df 100644 --- a/src/test/java/hudson/plugins/git/GitSCMTest.java +++ b/src/test/java/hudson/plugins/git/GitSCMTest.java @@ -1,67 +1,191 @@ package hudson.plugins.git; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; import com.google.common.collect.Lists; + import hudson.EnvVars; import hudson.FilePath; +import hudson.Launcher; +import hudson.matrix.Axis; +import hudson.matrix.AxisList; +import hudson.matrix.MatrixBuild; +import hudson.matrix.MatrixProject; import hudson.model.*; import hudson.plugins.git.GitSCM.BuildChooserContextImpl; +import hudson.plugins.git.GitSCM.DescriptorImpl; import hudson.plugins.git.browser.GitRepositoryBrowser; import hudson.plugins.git.browser.GithubWeb; import hudson.plugins.git.extensions.GitSCMExtension; -import hudson.plugins.git.extensions.impl.AuthorInChangelog; -import hudson.plugins.git.extensions.impl.CleanBeforeCheckout; -import hudson.plugins.git.extensions.impl.LocalBranch; -import hudson.plugins.git.extensions.impl.PreBuildMerge; -import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; -import hudson.plugins.git.extensions.impl.SparseCheckoutPath; -import hudson.plugins.git.extensions.impl.SparseCheckoutPaths; +import hudson.plugins.git.extensions.impl.*; +import hudson.plugins.git.util.BuildChooser; import hudson.plugins.git.util.BuildChooserContext; import hudson.plugins.git.util.BuildChooserContext.ContextCallable; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.GitUtils; import hudson.plugins.parameterizedtrigger.BuildTrigger; import hudson.plugins.parameterizedtrigger.ResultCondition; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; +import hudson.scm.ChangeLogSet; import hudson.scm.PollingResult; +import hudson.scm.PollingResult.Change; +import hudson.scm.SCMRevisionState; import hudson.slaves.DumbSlave; import hudson.slaves.EnvironmentVariablesNodeProperty.Entry; -import hudson.plugins.git.GitSCM.DescriptorImpl; +import hudson.tools.ToolLocationNodeProperty; import hudson.tools.ToolProperty; -import hudson.util.IOException2; - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; - +import hudson.triggers.SCMTrigger; +import hudson.util.LogTaskListener; +import hudson.util.RingBufferLogHandler; import hudson.util.StreamTaskListener; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; -import org.jenkinsci.plugins.gitclient.Git; -import org.jenkinsci.plugins.gitclient.GitClient; -import org.jenkinsci.plugins.gitclient.JGitTool; -import org.jenkinsci.plugins.gitclient.RepositoryCallback; -import org.jvnet.hudson.test.Bug; +import org.jenkinsci.plugins.tokenmacro.TokenMacro; +import org.jenkinsci.plugins.gitclient.*; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.TestExtension; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.net.URL; +import java.text.MessageFormat; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.jgit.transport.RemoteConfig; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.Assert.*; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import jenkins.model.Jenkins; +import jenkins.plugins.git.CliGitCommand; +import jenkins.plugins.git.GitSampleRepoRule; /** * Tests for {@link GitSCM}. * @author ishaaq */ public class GitSCMTest extends AbstractGitTestCase { + @Rule + public GitSampleRepoRule secondRepo = new GitSampleRepoRule(); + + private CredentialsStore store = null; + + @BeforeClass + public static void setGitDefaults() throws Exception { + CliGitCommand gitCmd = new CliGitCommand(null); + gitCmd.setDefaults(); + } + + @Before + public void enableSystemCredentialsProvider() throws Exception { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap( + Collections.singletonMap(Domain.global(), Collections.emptyList())); + for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) { + if (s.getProvider() instanceof SystemCredentialsProvider.ProviderImpl) { + store = s; + break; + + } + } + assertThat("The system credentials provider is enabled", store, notNullValue()); + } + + @After + public void waitForJenkinsIdle() throws Exception { + if (cleanupIsUnreliable()) { + rule.waitUntilNoActivityUpTo(5001); + } + } + + private StandardCredentials getInvalidCredential() { + String username = "bad-user"; + String password = "bad-password"; + CredentialsScope scope = CredentialsScope.GLOBAL; + String id = "username-" + username + "-password-" + password; + return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password); + } + + @Test + public void trackCredentials() throws Exception { + StandardCredentials credential = getInvalidCredential(); + store.addCredentials(Domain.global(), credential); + + Fingerprint fingerprint = CredentialsProvider.getFingerprintOf(credential); + assertThat("Fingerprint should not be set before job definition", fingerprint, nullValue()); + + JenkinsRule.WebClient wc = rule.createWebClient(); + HtmlPage page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId()); + assertThat("Have usage tracking reported", page.getElementById("usage"), notNullValue()); + assertThat("No fingerprint created until first use", page.getElementById("usage-missing"), notNullValue()); + assertThat("No fingerprint created until first use", page.getElementById("usage-present"), nullValue()); + + FreeStyleProject project = setupProject("master", credential); + + fingerprint = CredentialsProvider.getFingerprintOf(credential); + assertThat("Fingerprint should not be set before first build", fingerprint, nullValue()); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + build(project, Result.SUCCESS, commitFile1); + + fingerprint = CredentialsProvider.getFingerprintOf(credential); + assertThat("Fingerprint should be set after first build", fingerprint, notNullValue()); + assertThat(fingerprint.getJobs(), hasItem(is(project.getFullName()))); + Fingerprint.RangeSet rangeSet = fingerprint.getRangeSet(project); + assertThat(rangeSet, notNullValue()); + assertThat(rangeSet.includes(project.getLastBuild().getNumber()), is(true)); + + page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId()); + assertThat(page.getElementById("usage-missing"), nullValue()); + assertThat(page.getElementById("usage-present"), notNullValue()); + assertThat(page.getAnchorByText(project.getFullDisplayName()), notNullValue()); + } /** * Basic test - create a GitSCM based project, check it out and build for the first time. * Next test that polling works correctly, make another commit, check that polling finds it, * then build it and finally test the build culprits as well as the contents of the workspace. - * @throws Exception if an exception gets thrown. + * @throws Exception on error */ + @Test public void testBasic() throws Exception { FreeStyleProject project = setupSimpleProject("master"); @@ -81,10 +205,12 @@ public void testBasic() throws Exception { assertEquals("The build should have only one culprit", 1, culprits.size()); assertEquals("", janeDoe.getName(), culprits.iterator().next().getFullName()); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test + @Issue("JENKINS-56176") public void testBasicRemotePoll() throws Exception { // FreeStyleProject project = setupProject("master", true, false); FreeStyleProject project = setupProject("master", false, null, null, null, true, null); @@ -96,7 +222,7 @@ public void testBasicRemotePoll() throws Exception { assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); final String commitFile2 = "commitFile2"; - commit(commitFile2, janeDoe, "Commit number 2"); + String sha1String = commit(commitFile2, janeDoe, "Commit number 2"); assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); // ... and build it... final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); @@ -104,18 +230,90 @@ public void testBasicRemotePoll() throws Exception { assertEquals("The build should have only one culprit", 1, culprits.size()); assertEquals("", janeDoe.getName(), culprits.iterator().next().getFullName()); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + // JENKINS-56176 token macro expansion broke when BuildData was no longer updated + assertThat(TokenMacro.expandAll(build2, listener, "${GIT_REVISION,length=7}"), is(sha1String.substring(0, 7))); + assertThat(TokenMacro.expandAll(build2, listener, "${GIT_REVISION}"), is(sha1String)); + assertThat(TokenMacro.expandAll(build2, listener, "$GIT_REVISION"), is(sha1String)); } + @Test public void testBranchSpecWithRemotesMaster() throws Exception { FreeStyleProject projectMasterBranch = setupProject("remotes/origin/master", false, null, null, null, true, null); // create initial commit and build final String commitFile1 = "commitFile1"; commit(commitFile1, johnDoe, "Commit number 1"); build(projectMasterBranch, Result.SUCCESS, commitFile1); - } - + } + + /** + * This test and testSpecificRefspecsWithoutCloneOption confirm behaviors of + * refspecs on initial clone. Without the CloneOption to honor refspec, all + * references are cloned, even if they will be later ignored due to the + * refspec. With the CloneOption to ignore refspec, the initial clone also + * honors the refspec and only retrieves references per the refspec. + * @throws Exception on error + */ + @Test + @Issue("JENKINS-31393") + public void testSpecificRefspecs() throws Exception { + List repos = new ArrayList<>(); + repos.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", "+refs/heads/foo:refs/remotes/foo", null)); + + /* Set CloneOption to honor refspec on initial clone */ + FreeStyleProject projectWithMaster = setupProject(repos, Collections.singletonList(new BranchSpec("master")), null, false, null); + CloneOption cloneOptionMaster = new CloneOption(false, null, null); + cloneOptionMaster.setHonorRefspec(true); + ((GitSCM)projectWithMaster.getScm()).getExtensions().add(cloneOptionMaster); + + /* Set CloneOption to honor refspec on initial clone */ + FreeStyleProject projectWithFoo = setupProject(repos, Collections.singletonList(new BranchSpec("foo")), null, false, null); + CloneOption cloneOptionFoo = new CloneOption(false, null, null); + cloneOptionFoo.setHonorRefspec(true); + ((GitSCM)projectWithMaster.getScm()).getExtensions().add(cloneOptionFoo); + + // create initial commit + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit in master"); + // create branch and make initial commit + git.branch("foo"); + git.checkout().branch("foo"); + commit(commitFile1, johnDoe, "Commit in foo"); + + build(projectWithMaster, Result.FAILURE); + build(projectWithFoo, Result.SUCCESS, commitFile1); + } + + /** + * This test and testSpecificRefspecs confirm behaviors of + * refspecs on initial clone. Without the CloneOption to honor refspec, all + * references are cloned, even if they will be later ignored due to the + * refspec. With the CloneOption to ignore refspec, the initial clone also + * honors the refspec and only retrieves references per the refspec. + * @throws Exception on error + */ + @Test + @Issue("JENKINS-36507") + public void testSpecificRefspecsWithoutCloneOption() throws Exception { + List repos = new ArrayList<>(); + repos.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", "+refs/heads/foo:refs/remotes/foo", null)); + FreeStyleProject projectWithMaster = setupProject(repos, Collections.singletonList(new BranchSpec("master")), null, false, null); + FreeStyleProject projectWithFoo = setupProject(repos, Collections.singletonList(new BranchSpec("foo")), null, false, null); + + // create initial commit + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit in master"); + // create branch and make initial commit + git.branch("foo"); + git.checkout().branch("foo"); + commit(commitFile1, johnDoe, "Commit in foo"); + + build(projectWithMaster, Result.SUCCESS); /* If clone refspec had been honored, this would fail */ + build(projectWithFoo, Result.SUCCESS, commitFile1); + } + + @Test public void testBranchSpecWithRemotesHierarchical() throws Exception { FreeStyleProject projectMasterBranch = setupProject("master", false, null, null, null, true, null); FreeStyleProject projectHierarchicalBranch = setupProject("remotes/origin/rel-1/xy", false, null, null, null, true, null); @@ -130,6 +328,7 @@ public void testBranchSpecWithRemotesHierarchical() throws Exception { build(projectHierarchicalBranch, Result.SUCCESS, commitFile1); } + @Test public void testBranchSpecUsingTagWithSlash() throws Exception { FreeStyleProject projectMasterBranch = setupProject("path/tag", false, null, null, null, true, null); // create initial commit and build @@ -138,7 +337,8 @@ public void testBranchSpecUsingTagWithSlash() throws Exception { testRepo.git.tag("path/tag", "tag with a slash in the tag name"); build(projectMasterBranch, Result.SUCCESS, commitFile1); } - + + @Test public void testBasicIncludedRegion() throws Exception { FreeStyleProject project = setupProject("master", false, null, null, null, ".*3"); @@ -167,10 +367,309 @@ public void testBasicIncludedRegion() throws Exception { assertTrue(build2.getWorkspace().child(commitFile2).exists()); assertTrue(build2.getWorkspace().child(commitFile3).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } - + + /** + * testMergeCommitInExcludedRegionIsIgnored() confirms behavior of excluded regions with merge commits. + * This test has excluded and included regions, for files ending with .excluded and .included, + * respectively. The git repository is set up so that a non-fast-forward merge commit comes + * to master. The newly merged commit is a file ending with .excluded, so it should be ignored. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitInExcludedRegionIsIgnored() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, ".*\\.excluded", null, ".*\\.included"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "fileToMerge.excluded"; + commit(fileToMerge, johnDoe, "Commit should be ignored: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return false, because our merge commit falls within the excluded region. + assertFalse("Polling should report no changes, because they are in the excluded region.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitInExcludedDirectoryIsIgnored() confirms behavior of excluded directories with merge commits. + * This test has excluded and included directories, named /excluded/ and /included/,respectively. The repository + * is set up so that a non-fast-forward merge commit comes to master, and is in the directory /excluded/, + * so it should be ignored. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitInExcludedDirectoryIsIgnored() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, "excluded/.*", null, "included/.*"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "excluded/should-be-ignored"; + commit(fileToMerge, johnDoe, "Commit should be ignored: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return false, because our merge commit falls within the excluded directory. + assertFalse("Polling should see no changes, because they are in the excluded directory.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitInIncludedRegionIsProcessed() confirms behavior of included regions with merge commits. + * This test has excluded and included regions, for files ending with .excluded and .included, respectively. + * The git repository is set up so that a non-fast-forward merge commit comes to master. The newly merged + * commit is a file ending with .included, so it should be processed as a new change. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitInIncludedRegionIsProcessed() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, ".*\\.excluded", null, ".*\\.included"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "fileToMerge.included"; + commit(fileToMerge, johnDoe, "Commit should be noticed and processed as a change: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return true, because our commit falls within the included region. + assertTrue("Polling should report changes, because they fall within the included region.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitInIncludedRegionIsProcessed() confirms behavior of included directories with merge commits. + * This test has excluded and included directories, named /excluded/ and /included/, respectively. The repository + * is set up so that a non-fast-forward merge commit comes to master, and is in the directory /included/, + * so it should be processed as a new change. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitInIncludedDirectoryIsProcessed() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, "excluded/.*", null, "included/.*"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "included/should-be-processed"; + commit(fileToMerge, johnDoe, "Commit should be noticed and processed as a change: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // When this test passes, project.poll(listener).hasChanges()) should return + // true, because our commit falls within the included region. + assertTrue("Polling should report changes, because they are in the included directory.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitOutsideIncludedRegionIsIgnored() confirms behavior of included regions with merge commits. + * This test has an included region defined, for files ending with .included. There is no excluded region + * defined. The repository is set up and a non-fast-forward merge commit comes to master. The newly merged commit + * is a file ending with .should-be-ignored, thus falling outside of the included region, so it should ignored. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitOutsideIncludedRegionIsIgnored() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, null, null, ".*\\.included"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "fileToMerge.should-be-ignored"; + commit(fileToMerge, johnDoe, "Commit should be ignored: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return false, because our commit falls outside the included region. + assertFalse("Polling should ignore the change, because it falls outside the included region.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitOutsideIncludedDirectoryIsIgnored() confirms behavior of included directories with merge commits. + * This test has only an included directory `/included` defined. The git repository is set up so that + * a non-fast-forward, but mergeable, commit comes to master. The newly merged commit is outside of the + * /included/ directory, so polling should report no changes. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitOutsideIncludedDirectoryIsIgnored() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, null, null, "included/.*"); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "directory-to-ignore/file-should-be-ignored"; + commit(fileToMerge, johnDoe, "Commit should be ignored: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return false, because our commit falls outside of the included directory + assertFalse("Polling should ignore the change, because it falls outside the included directory.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitOutsideExcludedRegionIsProcessed() confirms behavior of excluded regions with merge commits. + * This test has an excluded region defined, for files ending with .excluded. There is no included region defined. + * The repository is set up so a non-fast-forward merge commit comes to master. The newly merged commit is a file + * ending with .should-be-processed, thus falling outside of the excluded region, so it should processed + * as a new change. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitOutsideExcludedRegionIsProcessed() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, ".*\\.excluded", null, null); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + final String fileToMerge = "fileToMerge.should-be-processed"; + commit(fileToMerge, johnDoe, "Commit should be noticed and processed as a change: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return true, because our commit falls outside of the excluded region + assertTrue("Polling should process the change, because it falls outside the excluded region.", + project.poll(listener).hasChanges()); + } + + /** + * testMergeCommitOutsideExcludedDirectoryIsProcessed() confirms behavior of excluded directories with merge commits. + * This test has an excluded directory `excluded` defined. There is no `included` directory defined. The repository + * is set up so that a non-fast-forward merge commit comes to master. The newly merged commit resides in a + * directory of its own, thus falling outside of the excluded directory, so it should processed + * as a new change. + * + * @throws Exception on error + */ + @Issue({"JENKINS-20389","JENKINS-23606"}) + @Test + public void testMergeCommitOutsideExcludedDirectoryIsProcessed() throws Exception { + final String branchToMerge = "new-branch-we-merge-to-master"; + + FreeStyleProject project = setupProject("master", false, null, "excluded/.*", null, null); + + final String initialCommit = "initialCommit"; + commit(initialCommit, johnDoe, "Commit " + initialCommit + " to master"); + build(project, Result.SUCCESS, initialCommit); + + final String secondCommit = "secondCommit"; + commit(secondCommit, johnDoe, "Commit " + secondCommit + " to master"); + + testRepo.git.checkoutBranch(branchToMerge, "HEAD~"); + // Create this new file outside of our excluded directory + final String fileToMerge = "directory-to-include/file-should-be-processed"; + commit(fileToMerge, johnDoe, "Commit should be noticed and processed as a change: " + fileToMerge + " to " + branchToMerge); + + ObjectId branchSHA = git.revParse("HEAD"); + testRepo.git.checkoutBranch("master", "refs/heads/master"); + MergeCommand mergeCommand = testRepo.git.merge(); + mergeCommand.setRevisionToMerge(branchSHA); + mergeCommand.execute(); + + // Should return true, because our commit falls outside of the excluded directory + assertTrue("SCM polling should process the change, because it falls outside the excluded directory.", + project.poll(listener).hasChanges()); + } + + @Test public void testIncludedRegionWithDeeperCommits() throws Exception { FreeStyleProject project = setupProject("master", false, null, null, null, ".*3"); @@ -203,10 +702,11 @@ public void testIncludedRegionWithDeeperCommits() throws Exception { assertTrue(build2.getWorkspace().child(commitFile2).exists()); assertTrue(build2.getWorkspace().child(commitFile3).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test public void testBasicExcludedRegion() throws Exception { FreeStyleProject project = setupProject("master", false, null, ".*2", null, null); @@ -234,10 +734,11 @@ public void testBasicExcludedRegion() throws Exception { assertTrue(build2.getWorkspace().child(commitFile2).exists()); assertTrue(build2.getWorkspace().child(commitFile3).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test public void testCleanBeforeCheckout() throws Exception { FreeStyleProject p = setupProject("master", false, null, null, "Jane Doe", null); ((GitSCM)p.getScm()).getExtensions().add(new CleanBeforeCheckout()); @@ -248,7 +749,7 @@ public void testCleanBeforeCheckout() throws Exception { final FreeStyleBuild firstBuild = build(p, Result.SUCCESS, commitFile1); final String branch1 = "Branch1"; final String branch2 = "Branch2"; - List branches = new ArrayList(); + List branches = new ArrayList<>(); branches.add(new BranchSpec("master")); branches.add(new BranchSpec(branch1)); branches.add(new BranchSpec(branch2)); @@ -268,7 +769,9 @@ public void testCleanBeforeCheckout() throws Exception { } - @Bug(value = 8342) + + @Issue("JENKINS-8342") + @Test public void testExcludedRegionMultiCommit() throws Exception { // Got 2 projects, each one should only build if changes in its own file FreeStyleProject clientProject = setupProject("master", false, null, ".*serverFile", null, null); @@ -295,12 +798,14 @@ public void testExcludedRegionMultiCommit() throws Exception { assertTrue("scm polling did not detect changes in server project", serverProject.poll(listener).hasChanges()); } - /** + /* * With multiple branches specified in the project and having commits from a user * excluded should not build the excluded revisions when another branch changes. */ - @Bug(value = 8342) - public void testMultipleBranchWithExcludedUser() throws Exception { /* + /* + @Issue("JENKINS-8342") + @Test + public void testMultipleBranchWithExcludedUser() throws Exception { final String branch1 = "Branch1"; final String branch2 = "Branch2"; @@ -366,8 +871,9 @@ public void testMultipleBranchWithExcludedUser() throws Exception { /* assertTrue("scm polling should detect changes in 'Branch1' branch", project.poll(listener).hasChanges()); build(project, Result.SUCCESS, branch1File1, branch1File2, branch1File3); - */ } + } */ + @Test public void testBasicExcludedUser() throws Exception { FreeStyleProject project = setupProject("master", false, null, null, "Jane Doe", null); @@ -394,11 +900,12 @@ public void testBasicExcludedUser() throws Exception { assertTrue(build2.getWorkspace().child(commitFile2).exists()); assertTrue(build2.getWorkspace().child(commitFile3).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test public void testBasicInSubdir() throws Exception { FreeStyleProject project = setupSimpleProject("master"); ((GitSCM)project.getScm()).getExtensions().add(new RelativeTargetDirectory("subdir")); @@ -423,13 +930,14 @@ public void testBasicInSubdir() throws Exception { build2.getWorkspace().child("subdir").exists()); assertEquals("The 'subdir' subdirectory should contain commitFile2, but does not.", true, build2.getWorkspace().child("subdir").child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } - public void testBasicWithSlave() throws Exception { + @Test + public void testBasicWithAgent() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - project.setAssignedLabel(createSlave().getSelfLabel()); + project.setAssignedLabel(rule.createSlave().getSelfLabel()); // create initial commit and then run the build against it: final String commitFile1 = "commitFile1"; @@ -447,18 +955,18 @@ public void testBasicWithSlave() throws Exception { assertEquals("The build should have only one culprit", 1, culprits.size()); assertEquals("", janeDoe.getName(), culprits.iterator().next().getFullName()); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } - // For HUDSON-7547 - public void testBasicWithSlaveNoExecutorsOnMaster() throws Exception { + @Issue("HUDSON-7547") + @Test + public void testBasicWithAgentNoExecutorsOnMaster() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - hudson.setNumExecutors(0); - hudson.setNodes(hudson.getNodes()); + rule.jenkins.setNumExecutors(0); - project.setAssignedLabel(createSlave().getSelfLabel()); + project.setAssignedLabel(rule.createSlave().getSelfLabel()); // create initial commit and then run the build against it: final String commitFile1 = "commitFile1"; @@ -476,10 +984,11 @@ public void testBasicWithSlaveNoExecutorsOnMaster() throws Exception { assertEquals("The build should have only one culprit", 1, culprits.size()); assertEquals("", janeDoe.getName(), culprits.iterator().next().getFullName()); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test public void testAuthorOrCommitterFalse() throws Exception { // Test with authorOrCommitter set to false and make sure we get the committer. FreeStyleProject project = setupSimpleProject("master"); @@ -502,10 +1011,11 @@ public void testAuthorOrCommitterFalse() throws Exception { final Set secondCulprits = secondBuild.getCulprits(); assertEquals("The build should have only one culprit", 1, secondCulprits.size()); - assertEquals("Did not get the committer as the change author with authorOrCommiter==false", + assertEquals("Did not get the committer as the change author with authorOrCommitter==false", janeDoe.getName(), secondCulprits.iterator().next().getFullName()); } + @Test public void testAuthorOrCommitterTrue() throws Exception { // Next, test with authorOrCommitter set to true and make sure we get the author. FreeStyleProject project = setupSimpleProject("master"); @@ -529,13 +1039,11 @@ public void testAuthorOrCommitterTrue() throws Exception { final Set secondCulprits = secondBuild.getCulprits(); assertEquals("The build should have only one culprit", 1, secondCulprits.size()); - assertEquals("Did not get the author as the change author with authorOrCommiter==true", + assertEquals("Did not get the author as the change author with authorOrCommitter==true", johnDoe.getName(), secondCulprits.iterator().next().getFullName()); } - /** - * Method name is self-explanatory. - */ + @Test public void testNewCommitToUntrackedBranchDoesNotTriggerBuild() throws Exception { FreeStyleProject project = setupSimpleProject("master"); @@ -552,33 +1060,72 @@ public void testNewCommitToUntrackedBranchDoesNotTriggerBuild() throws Exception assertFalse("scm polling should not detect commit2 change because it is not in the branch we are tracking.", project.poll(listener).hasChanges()); } - public void testBranchIsAvailableInEvironment() throws Exception { + private String checkoutString(FreeStyleProject project, String envVar) { + return "checkout -f " + getEnvVars(project).get(envVar); + } + + @Test + public void testEnvVarsAvailable() throws Exception { FreeStyleProject project = setupSimpleProject("master"); final String commitFile1 = "commitFile1"; commit(commitFile1, johnDoe, "Commit number 1"); - build(project, Result.SUCCESS, commitFile1); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); assertEquals("origin/master", getEnvVars(project).get(GitSCM.GIT_BRANCH)); + rule.assertLogContains(getEnvVars(project).get(GitSCM.GIT_BRANCH), build1); + + rule.assertLogContains(checkoutString(project, GitSCM.GIT_COMMIT), build1); + + final String commitFile2 = "commitFile2"; + commit(commitFile2, johnDoe, "Commit number 2"); + FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); + + rule.assertLogNotContains(checkoutString(project, GitSCM.GIT_PREVIOUS_COMMIT), build2); + rule.assertLogContains(checkoutString(project, GitSCM.GIT_PREVIOUS_COMMIT), build1); + + rule.assertLogNotContains(checkoutString(project, GitSCM.GIT_PREVIOUS_SUCCESSFUL_COMMIT), build2); + rule.assertLogContains(checkoutString(project, GitSCM.GIT_PREVIOUS_SUCCESSFUL_COMMIT), build1); } - // For HUDSON-7411 + @Issue("HUDSON-7411") + @Test public void testNodeEnvVarsAvailable() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - Node s = createSlave(); - setVariables(s, new Entry("TESTKEY", "slaveValue")); - project.setAssignedLabel(s.getSelfLabel()); + DumbSlave agent = rule.createSlave(); + setVariables(agent, new Entry("TESTKEY", "agent value")); + project.setAssignedLabel(agent.getSelfLabel()); final String commitFile1 = "commitFile1"; commit(commitFile1, johnDoe, "Commit number 1"); build(project, Result.SUCCESS, commitFile1); - assertEquals("slaveValue", getEnvVars(project).get("TESTKEY")); + assertEquals("agent value", getEnvVars(project).get("TESTKEY")); } - /** + @Test + public void testNodeOverrideGit() throws Exception { + GitSCM scm = new GitSCM(null); + + DumbSlave agent = rule.createSlave(); + GitTool.DescriptorImpl gitToolDescriptor = rule.jenkins.getDescriptorByType(GitTool.DescriptorImpl.class); + GitTool installation = new GitTool("Default", "/usr/bin/git", null); + gitToolDescriptor.setInstallations(installation); + + String gitExe = scm.getGitExe(agent, TaskListener.NULL); + assertEquals("/usr/bin/git", gitExe); + + ToolLocationNodeProperty nodeGitLocation = new ToolLocationNodeProperty(new ToolLocationNodeProperty.ToolLocation(gitToolDescriptor, "Default", "C:\\Program Files\\Git\\bin\\git.exe")); + agent.setNodeProperties(Collections.singletonList(nodeGitLocation)); + + gitExe = scm.getGitExe(agent, TaskListener.NULL); + assertEquals("C:\\Program Files\\Git\\bin\\git.exe", gitExe); + } + + /* * A previous version of GitSCM would only build against branches, not tags. This test checks that that * regression has been fixed. */ + @Test public void testGitSCMCanBuildAgainstTags() throws Exception { final String mytag = "mytag"; FreeStyleProject project = setupSimpleProject(mytag); @@ -632,10 +1179,11 @@ public void testGitSCMCanBuildAgainstTags() throws Exception { assertFalse("scm polling should not detect any more changes after last build", project.poll(listener).hasChanges()); } - /** + /* * Not specifying a branch string in the project implies that we should be polling for changes in * all branches. */ + @Test public void testMultipleBranchBuild() throws Exception { // empty string will result in a project that tracks against changes in all branches: final FreeStyleProject project = setupSimpleProject(""); @@ -668,15 +1216,51 @@ public void testMultipleBranchBuild() throws Exception { assertFalse("scm polling should not detect any more changes after last build", project.poll(listener).hasChanges()); } - @Bug(19037) + @Test + public void testMultipleBranchesWithTags() throws Exception { + List branchSpecs = Arrays.asList( + new BranchSpec("refs/tags/v*"), + new BranchSpec("refs/remotes/origin/non-existent")); + FreeStyleProject project = setupProject(branchSpecs, false, null, null, janeDoe.getName(), null, false, null); + + // create initial commit and then run the build against it: + // Here the changelog is by default empty (because changelog for first commit is always empty + commit("commitFileBase", johnDoe, "Initial Commit"); + + // there are no branches to be build + FreeStyleBuild freeStyleBuild = build(project, Result.FAILURE); + + final String v1 = "v1"; + + git.tag(v1, "version 1"); + assertTrue("v1 tag exists", git.tagExists(v1)); + + freeStyleBuild = build(project, Result.SUCCESS); + assertTrue("change set is empty", freeStyleBuild.getChangeSet().isEmptySet()); + + commit("file1", johnDoe, "change to file1"); + git.tag("none", "latest"); + + freeStyleBuild = build(project, Result.SUCCESS); + + ObjectId tag = git.revParse(Constants.R_TAGS + v1); + GitSCM scm = (GitSCM)project.getScm(); + BuildData buildData = scm.getBuildData(freeStyleBuild); + + assertEquals("last build matches the v1 tag revision", tag, buildData.lastBuild.getSHA1()); + } + + @Issue("JENKINS-19037") @SuppressWarnings("ResultOfObjectAllocationIgnored") + @Test public void testBlankRepositoryName() throws Exception { new GitSCM(null); } - @Bug(10060) + @Issue("JENKINS-10060") + @Test public void testSubmoduleFixup() throws Exception { - File repo = createTmpDir(); + File repo = secondRepo.getRoot(); FilePath moduleWs = new FilePath(repo); org.jenkinsci.plugins.gitclient.GitClient moduleRepo = Git.with(listener, new EnvVars()).in(repo).getClient(); @@ -699,41 +1283,41 @@ public void testSubmoduleFixup() throws Exception { new GitRevisionBuildParameters()))); d.setScm(new GitSCM(workDir.getPath())); - hudson.rebuildDependencyGraph(); + rule.jenkins.rebuildDependencyGraph(); - FreeStyleBuild ub = assertBuildStatusSuccess(u.scheduleBuild2(0)); - System.out.println(ub.getLog()); + FreeStyleBuild ub = rule.assertBuildStatusSuccess(u.scheduleBuild2(0)); for (int i=0; (d.getLastBuild()==null || d.getLastBuild().isBuilding()) && i<100; i++) // wait only up to 10 sec to avoid infinite loop Thread.sleep(100); FreeStyleBuild db = d.getLastBuild(); assertNotNull("downstream build didn't happen",db); - assertBuildStatusSuccess(db); + rule.assertBuildStatusSuccess(db); } + @Test public void testBuildChooserContext() throws Exception { final FreeStyleProject p = createFreeStyleProject(); - final FreeStyleBuild b = assertBuildStatusSuccess(p.scheduleBuild2(0)); + final FreeStyleBuild b = rule.assertBuildStatusSuccess(p.scheduleBuild2(0)); BuildChooserContextImpl c = new BuildChooserContextImpl(p, b, null); - c.actOnBuild(new ContextCallable, Object>() { - public Object invoke(AbstractBuild param, VirtualChannel channel) throws IOException, InterruptedException { + c.actOnBuild(new ContextCallable, Object>() { + public Object invoke(Run param, VirtualChannel channel) throws IOException, InterruptedException { assertSame(param,b); return null; } }); - c.actOnProject(new ContextCallable, Object>() { - public Object invoke(AbstractProject param, VirtualChannel channel) throws IOException, InterruptedException { + c.actOnProject(new ContextCallable, Object>() { + public Object invoke(Job param, VirtualChannel channel) throws IOException, InterruptedException { assertSame(param,p); return null; } }); - DumbSlave s = createOnlineSlave(); - assertEquals(p.toString(), s.getChannel().call(new BuildChooserContextTestCallable(c))); + DumbSlave agent = rule.createOnlineSlave(); + assertEquals(p.toString(), agent.getChannel().call(new BuildChooserContextTestCallable(c))); } - private static class BuildChooserContextTestCallable implements Callable { + private static class BuildChooserContextTestCallable extends MasterToSlaveCallable { private final BuildChooserContext c; public BuildChooserContextTestCallable(BuildChooserContext c) { @@ -742,17 +1326,18 @@ public BuildChooserContextTestCallable(BuildChooserContext c) { public String call() throws IOException { try { - return c.actOnProject(new ContextCallable, String>() { - public String invoke(AbstractProject param, VirtualChannel channel) throws IOException, InterruptedException { + return c.actOnProject(new ContextCallable, String>() { + public String invoke(Job param, VirtualChannel channel) throws IOException, InterruptedException { assertTrue(channel instanceof Channel); - assertTrue(Hudson.getInstance()!=null); + assertTrue(Jenkins.getInstanceOrNull()!=null); return param.toString(); } }); } catch (InterruptedException e) { - throw new IOException2(e); + throw new IOException(e); } } + } // eg: "jane doe and john doe should be the culprits", culprits, [johnDoe, janeDoe]) @@ -771,12 +1356,20 @@ public String apply(User u) } } + @Test public void testEmailCommitter() throws Exception { FreeStyleProject project = setupSimpleProject("master"); // setup global config - final DescriptorImpl descriptor = (DescriptorImpl) project.getScm().getDescriptor(); + GitSCM scm = (GitSCM) project.getScm(); + final DescriptorImpl descriptor = (DescriptorImpl) scm.getDescriptor(); + assertFalse("Wrong initial value for create account based on e-mail", scm.isCreateAccountBasedOnEmail()); descriptor.setCreateAccountBasedOnEmail(true); + assertTrue("Create account based on e-mail not set", scm.isCreateAccountBasedOnEmail()); + + assertFalse("Wrong initial value for use existing user if same e-mail already found", scm.isUseExistingAccountWithSameEmail()); + descriptor.setUseExistingAccountWithSameEmail(true); + assertTrue("Use existing user if same e-mail already found is not set", scm.isUseExistingAccountWithSameEmail()); // create initial commit and then run the build against it: final String commitFile1 = "commitFile1"; @@ -800,14 +1393,50 @@ public void testEmailCommitter() throws Exception { assertEquals("", jeffDoe.getEmailAddress(), culprit.getId()); assertEquals("", jeffDoe.getName(), culprit.getFullName()); - assertBuildStatusSuccess(build); + rule.assertBuildStatusSuccess(build); } + + @Issue("JENKINS-59868") + @Test + public void testNonExistentWorkingDirectoryPoll() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + // create initial commit and then run the build against it + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + project.setScm(new GitSCM( + ((GitSCM)project.getScm()).getUserRemoteConfigs(), + Collections.singletonList(new BranchSpec("master")), + false, Collections.emptyList(), + null, null, + // configure GitSCM with the DisableRemotePoll extension to ensure that polling use the workspace + Collections.singletonList(new DisableRemotePoll()))); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + + // Empty the workspace directory + build1.getWorkspace().deleteRecursive(); + + // Setup a recorder for polling logs + RingBufferLogHandler pollLogHandler = new RingBufferLogHandler(10); + Logger pollLogger = Logger.getLogger(GitSCMTest.class.getName()); + pollLogger.addHandler(pollLogHandler); + TaskListener taskListener = new LogTaskListener(pollLogger, Level.INFO); + + // Make sure that polling returns BUILD_NOW and properly log the reason + FilePath filePath = build1.getWorkspace(); + assertThat(project.getScm().compareRemoteRevisionWith(project, new Launcher.LocalLauncher(taskListener), + filePath, taskListener, null), is(PollingResult.BUILD_NOW)); + assertTrue(pollLogHandler.getView().stream().anyMatch(m -> + m.getMessage().contains("[poll] Working Directory does not exist"))); + } + + // Disabled - consistently fails, needs more analysis + // @Test public void testFetchFromMultipleRepositories() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - TestGitRepo secondTestRepo = new TestGitRepo("second", this, listener); - List remotes = new ArrayList(); + TestGitRepo secondTestRepo = new TestGitRepo("second", secondRepo.getRoot(), listener); + List remotes = new ArrayList<>(); remotes.addAll(testRepo.remoteConfigs()); remotes.addAll(secondTestRepo.remoteConfigs()); @@ -823,7 +1452,12 @@ public void testFetchFromMultipleRepositories() throws Exception { commit(commitFile1, johnDoe, "Commit number 1"); build(project, Result.SUCCESS, commitFile1); - assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + /* Diagnostic help - for later use */ + SCMRevisionState baseline = project.poll(listener).baseline; + Change change = project.poll(listener).change; + SCMRevisionState remote = project.poll(listener).remote; + String assertionMessage = MessageFormat.format("polling incorrectly detected change after build. Baseline: {0}, Change: {1}, Remote: {2}", baseline, change, remote); + assertFalse(assertionMessage, project.poll(listener).hasChanges()); final String commitFile2 = "commitFile2"; secondTestRepo.commit(commitFile2, janeDoe, "Commit number 2"); @@ -831,10 +1465,95 @@ public void testFetchFromMultipleRepositories() throws Exception { //... and build it... final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + private void branchSpecWithMultipleRepositories(String branchName) throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + TestGitRepo secondTestRepo = new TestGitRepo("second", secondRepo.getRoot(), listener); + List remotes = new ArrayList<>(); + remotes.addAll(testRepo.remoteConfigs()); + remotes.addAll(secondTestRepo.remoteConfigs()); + + // create initial commit + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + + project.setScm(new GitSCM( + remotes, + Collections.singletonList(new BranchSpec(branchName)), + false, Collections.emptyList(), + null, null, + Collections.emptyList())); + + final FreeStyleBuild build = build(project, Result.SUCCESS, commitFile1); + rule.assertBuildStatusSuccess(build); + } + + @Issue("JENKINS-26268") + public void testBranchSpecAsSHA1WithMultipleRepositories() throws Exception { + branchSpecWithMultipleRepositories(testRepo.git.revParse("HEAD").getName()); + } + + @Issue("JENKINS-26268") + public void testBranchSpecAsRemotesOriginMasterWithMultipleRepositories() throws Exception { + branchSpecWithMultipleRepositories("remotes/origin/master"); + } + + @Issue("JENKINS-25639") + @Test + public void testCommitDetectedOnlyOnceInMultipleRepositories() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + TestGitRepo secondTestRepo = new TestGitRepo("secondRepo", secondRepo.getRoot(), listener); + List remotes = new ArrayList<>(); + remotes.addAll(testRepo.remoteConfigs()); + remotes.addAll(secondTestRepo.remoteConfigs()); + + GitSCM gitSCM = new GitSCM( + remotes, + Collections.singletonList(new BranchSpec("origin/master")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(gitSCM); + + /* Check that polling would force build through + * compareRemoteRevisionWith by detecting no last build */ + FilePath filePath = new FilePath(new File(".")); + assertThat(gitSCM.compareRemoteRevisionWith(project, new Launcher.LocalLauncher(listener), filePath, listener, null), is(PollingResult.BUILD_NOW)); + + commit("commitFile1", johnDoe, "Commit number 1"); + FreeStyleBuild build = build(project, Result.SUCCESS, "commitFile1"); + + commit("commitFile2", johnDoe, "Commit number 2"); + git = Git.with(listener, new EnvVars()).in(build.getWorkspace()).getClient(); + for (RemoteConfig remoteConfig : gitSCM.getRepositories()) { + git.fetch_().from(remoteConfig.getURIs().get(0), remoteConfig.getFetchRefSpecs()); + } + BuildChooser buildChooser = gitSCM.getBuildChooser(); + Collection candidateRevisions = buildChooser.getCandidateRevisions(false, "origin/master", git, listener, project.getLastBuild().getAction(BuildData.class), null); + assertEquals(1, candidateRevisions.size()); + gitSCM.setBuildChooser(buildChooser); // Should be a no-op + Collection candidateRevisions2 = buildChooser.getCandidateRevisions(false, "origin/master", git, listener, project.getLastBuild().getAction(BuildData.class), null); + assertThat(candidateRevisions2, is(candidateRevisions)); + } + + private final Random random = new Random(); + private boolean useChangelogToBranch = random.nextBoolean(); + + private void addChangelogToBranchExtension(GitSCM scm) { + if (useChangelogToBranch) { + /* Changelog should be no different with this enabled or disabled */ + ChangelogToBranchOptions changelogOptions = new ChangelogToBranchOptions("origin", "master"); + scm.getExtensions().add(new ChangelogToBranch(changelogOptions)); + } + useChangelogToBranch = !useChangelogToBranch; + } + + @Test public void testMerge() throws Exception { FreeStyleProject project = setupSimpleProject("master"); @@ -844,7 +1563,8 @@ public void testMerge() throws Exception { false, Collections.emptyList(), null, null, Collections.emptyList()); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", "default"))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", "default", MergeCommand.GitPluginFastForwardMode.FF))); + addChangelogToBranchExtension(scm); project.setScm(scm); // create initial commit and then run the build against it: @@ -869,13 +1589,14 @@ public void testMerge() throws Exception { assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } - public void testMergeWithSlave() throws Exception { + @Issue("JENKINS-20392") + @Test + public void testMergeChangelog() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - project.setAssignedLabel(createSlave().getSelfLabel()); GitSCM scm = new GitSCM( createRemoteRepositories(), @@ -883,7 +1604,44 @@ public void testMergeWithSlave() throws Exception { false, Collections.emptyList(), null, null, Collections.emptyList()); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", "default", MergeCommand.GitPluginFastForwardMode.FF))); + addChangelogToBranchExtension(scm); + project.setScm(scm); + + // create initial commit and then run the build against it: + // Here the changelog is by default empty (because changelog for first commit is always empty + commit("commitFileBase", johnDoe, "Initial Commit"); + testRepo.git.branch("integration"); + build(project, Result.SUCCESS, "commitFileBase"); + + // Create second commit and run build + // Here the changelog should contain exactly this one new commit + testRepo.git.checkout("master", "topic2"); + final String commitFile2 = "commitFile2"; + String commitMessage = "Commit number 2"; + commit(commitFile2, johnDoe, commitMessage); + final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); + + ChangeLogSet changeLog = build2.getChangeSet(); + assertEquals("Changelog should contain one item", 1, changeLog.getItems().length); + + GitChangeSet singleChange = (GitChangeSet) changeLog.getItems()[0]; + assertEquals("Changelog should contain commit number 2", commitMessage, singleChange.getComment().trim()); + } + + @Test + public void testMergeWithAgent() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + project.setAssignedLabel(rule.createSlave().getSelfLabel()); + + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, null))); + addChangelogToBranchExtension(scm); project.setScm(scm); // create initial commit and then run the build against it: @@ -908,10 +1666,11 @@ public void testMergeWithSlave() throws Exception { assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); final FreeStyleBuild build2 = build(project, Result.SUCCESS, commitFile2); assertTrue(build2.getWorkspace().child(commitFile2).exists()); - assertBuildStatusSuccess(build2); + rule.assertBuildStatusSuccess(build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + @Test public void testMergeFailed() throws Exception { FreeStyleProject project = setupSimpleProject("master"); @@ -922,7 +1681,8 @@ public void testMergeFailed() throws Exception { null, null, Collections.emptyList()); project.setScm(scm); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", ""))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", "", MergeCommand.GitPluginFastForwardMode.FF))); + addChangelogToBranchExtension(scm); // create initial commit and then run the build against it: commit("commitFileBase", johnDoe, "Initial Commit"); @@ -944,13 +1704,47 @@ public void testMergeFailed() throws Exception { commit(commitFile1, "other content", johnDoe, "Commit number 2"); assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); final FreeStyleBuild build2 = build(project, Result.FAILURE); - assertBuildStatus(Result.FAILURE, build2); + rule.assertBuildStatus(Result.FAILURE, build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + + @Issue("JENKINS-25191") + @Test + public void testMultipleMergeFailed() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("master")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration1", "", MergeCommand.GitPluginFastForwardMode.FF))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration2", "", MergeCommand.GitPluginFastForwardMode.FF))); + addChangelogToBranchExtension(scm); + + commit("dummyFile", johnDoe, "Initial Commit"); + testRepo.git.branch("integration1"); + testRepo.git.branch("integration2"); + build(project, Result.SUCCESS); + + final String commitFile = "commitFile"; + testRepo.git.checkoutBranch("integration1","master"); + commit(commitFile,"abc", johnDoe, "merge conflict with integration2"); + + testRepo.git.checkoutBranch("integration2","master"); + commit(commitFile,"cde", johnDoe, "merge conflict with integration1"); + + final FreeStyleBuild build = build(project, Result.FAILURE); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + } - public void testMergeFailedWithSlave() throws Exception { + @Test + public void testMergeFailedWithAgent() throws Exception { FreeStyleProject project = setupSimpleProject("master"); - project.setAssignedLabel(createSlave().getSelfLabel()); + project.setAssignedLabel(rule.createSlave().getSelfLabel()); GitSCM scm = new GitSCM( createRemoteRepositories(), @@ -958,7 +1752,8 @@ public void testMergeFailedWithSlave() throws Exception { false, Collections.emptyList(), null, null, Collections.emptyList()); - scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null))); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, null))); + addChangelogToBranchExtension(scm); project.setScm(scm); // create initial commit and then run the build against it: @@ -981,10 +1776,56 @@ public void testMergeFailedWithSlave() throws Exception { commit(commitFile1, "other content", johnDoe, "Commit number 2"); assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); final FreeStyleBuild build2 = build(project, Result.FAILURE); - assertBuildStatus(Result.FAILURE, build2); + rule.assertBuildStatus(Result.FAILURE, build2); assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); } + + @Test + public void testMergeWithMatrixBuild() throws Exception { + + //Create a matrix project and a couple of axes + MatrixProject project = rule.jenkins.createProject(MatrixProject.class, "xyz"); + project.setAxes(new AxisList(new Axis("VAR","a","b"))); + + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("*")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(new PreBuildMerge(new UserMergeOptions("origin", "integration", null, null))); + addChangelogToBranchExtension(scm); + project.setScm(scm); + + // create initial commit and then run the build against it: + commit("commitFileBase", johnDoe, "Initial Commit"); + testRepo.git.branch("integration"); + build(project, Result.SUCCESS, "commitFileBase"); + + + testRepo.git.checkout(null, "topic1"); + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + final MatrixBuild build1 = build(project, Result.SUCCESS, commitFile1); + assertTrue(build1.getWorkspace().child(commitFile1).exists()); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + // do what the GitPublisher would do + testRepo.git.deleteBranch("integration"); + testRepo.git.checkout("topic1", "integration"); + + testRepo.git.checkout("master", "topic2"); + final String commitFile2 = "commitFile2"; + commit(commitFile2, johnDoe, "Commit number 2"); + assertTrue("scm polling did not detect commit2 change", project.poll(listener).hasChanges()); + final MatrixBuild build2 = build(project, Result.SUCCESS, commitFile2); + assertTrue(build2.getWorkspace().child(commitFile2).exists()); + rule.assertBuildStatusSuccess(build2); + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + } + + @Test public void testEnvironmentVariableExpansion() throws Exception { FreeStyleProject project = createFreeStyleProject(); project.setScm(new GitSCM("${CAT}"+testRepo.gitDir.getPath())); @@ -1014,15 +1855,16 @@ public void buildEnvironmentFor(Run r, EnvVars envs, TaskListener listener) thro } private List createRepoList(String url) { - List repoList = new ArrayList(); + List repoList = new ArrayList<>(); repoList.add(new UserRemoteConfig(url, null, null, null)); return repoList; } - /** + /* * Makes sure that git browser URL is preserved across config round trip. */ - @Bug(22604) + @Issue("JENKINS-22604") + @Test public void testConfigRoundtripURLPreserved() throws Exception { FreeStyleProject p = createFreeStyleProject(); final String url = "https://github.com/jenkinsci/jenkins"; @@ -1032,30 +1874,77 @@ public void testConfigRoundtripURLPreserved() throws Exception { false, Collections.emptyList(), browser, null, null); p.setScm(scm); - configRoundtrip(p); - assertEqualDataBoundBeans(scm,p.getScm()); + rule.configRoundtrip(p); + rule.assertEqualDataBoundBeans(scm,p.getScm()); + assertEquals("Wrong key", "git " + url, scm.getKey()); } - /** + /* + * Makes sure that git extensions are preserved across config round trip. + */ + @Issue("JENKINS-33695") + @Test + public void testConfigRoundtripExtensionsPreserved() throws Exception { + FreeStyleProject p = createFreeStyleProject(); + final String url = "git://github.com/jenkinsci/git-plugin.git"; + GitRepositoryBrowser browser = new GithubWeb(url); + GitSCM scm = new GitSCM(createRepoList(url), + Collections.singletonList(new BranchSpec("*/master")), + false, Collections.emptyList(), + browser, null, null); + p.setScm(scm); + + /* Assert that no extensions are loaded initially */ + assertEquals(Collections.emptyList(), scm.getExtensions().toList()); + + /* Add LocalBranch extension */ + LocalBranch localBranchExtension = new LocalBranch("**"); + scm.getExtensions().add(localBranchExtension); + assertTrue(scm.getExtensions().toList().contains(localBranchExtension)); + + /* Save the configuration */ + rule.configRoundtrip(p); + List extensions = scm.getExtensions().toList();; + assertTrue(extensions.contains(localBranchExtension)); + assertEquals("Wrong extension count before reload", 1, extensions.size()); + + /* Reload configuration from disc */ + p.doReload(); + GitSCM reloadedGit = (GitSCM) p.getScm(); + List reloadedExtensions = reloadedGit.getExtensions().toList(); + assertEquals("Wrong extension count after reload", 1, reloadedExtensions.size()); + LocalBranch reloadedLocalBranch = (LocalBranch) reloadedExtensions.get(0); + assertEquals(localBranchExtension.getLocalBranch(), reloadedLocalBranch.getLocalBranch()); + } + + /* * Makes sure that the configuration form works. */ + @Test public void testConfigRoundtrip() throws Exception { FreeStyleProject p = createFreeStyleProject(); GitSCM scm = new GitSCM("https://github.com/jenkinsci/jenkins"); p.setScm(scm); - configRoundtrip(p); - assertEqualDataBoundBeans(scm,p.getScm()); + rule.configRoundtrip(p); + rule.assertEqualDataBoundBeans(scm,p.getScm()); } - /** + /* * Sample configuration that should result in no extensions at all */ + @Test public void testDataCompatibility1() throws Exception { - FreeStyleProject p = (FreeStyleProject) jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("GitSCMTest/old1.xml")); - GitSCM git = (GitSCM) p.getScm(); - assertEquals(Collections.emptyList(), git.getExtensions().toList()); + FreeStyleProject p = (FreeStyleProject) rule.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("GitSCMTest/old1.xml")); + GitSCM oldGit = (GitSCM) p.getScm(); + assertEquals(Collections.emptyList(), oldGit.getExtensions().toList()); + assertEquals(0, oldGit.getSubmoduleCfg().size()); + assertEquals("git git://github.com/jenkinsci/model-ant-project.git", oldGit.getKey()); + assertThat(oldGit.getEffectiveBrowser(), instanceOf(GithubWeb.class)); + GithubWeb browser = (GithubWeb) oldGit.getEffectiveBrowser(); + assertEquals(browser.getRepoUrl(), "https://github.com/jenkinsci/model-ant-project.git/"); } + @Test public void testPleaseDontContinueAnyway() throws Exception { // create an empty repository with some commits testRepo.commit("a","foo",johnDoe, "added"); @@ -1063,26 +1952,27 @@ public void testPleaseDontContinueAnyway() throws Exception { FreeStyleProject p = createFreeStyleProject(); p.setScm(new GitSCM(testRepo.gitDir.getAbsolutePath())); - assertBuildStatusSuccess(p.scheduleBuild2(0)); + rule.assertBuildStatusSuccess(p.scheduleBuild2(0)); // this should fail as it fails to fetch - p.setScm(new GitSCM("http://www.google.com/no/such/repository.git")); - assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + p.setScm(new GitSCM("http://localhost:4321/no/such/repository.git")); + rule.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); } - @Bug(19108) + @Issue("JENKINS-19108") + @Test public void testCheckoutToSpecificBranch() throws Exception { FreeStyleProject p = createFreeStyleProject(); - GitSCM git = new GitSCM("https://github.com/imod/dummy-tester.git"); - setupJGit(git); - git.getExtensions().add(new LocalBranch("master")); - p.setScm(git); + GitSCM oldGit = new GitSCM("https://github.com/jenkinsci/model-ant-project.git/"); + setupJGit(oldGit); + oldGit.getExtensions().add(new LocalBranch("master")); + p.setScm(oldGit); - FreeStyleBuild b = assertBuildStatusSuccess(p.scheduleBuild2(0)); + FreeStyleBuild b = rule.assertBuildStatusSuccess(p.scheduleBuild2(0)); GitClient gc = Git.with(StreamTaskListener.fromStdout(),null).in(b.getWorkspace()).getClient(); gc.withRepository(new RepositoryCallback() { public Void invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { - Ref head = repo.getRef("HEAD"); + Ref head = repo.findRef("HEAD"); assertTrue("Detached HEAD",head.isSymbolic()); Ref t = head.getTarget(); assertEquals(t.getName(),"refs/heads/master"); @@ -1091,7 +1981,103 @@ public Void invoke(Repository repo, VirtualChannel channel) throws IOException, } }); } + + /** + * Verifies that if project specifies LocalBranch with value of "**" + * that the checkout to a local branch using remote branch name sans 'origin'. + * This feature is necessary to support Maven release builds that push updated + * pom.xml to remote branch as + *
        +     * git push origin localbranch:localbranch
        +     * 
        + * @throws Exception on error + */ + @Test + public void testCheckoutToDefaultLocalBranch_StarStar() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + GitSCM git = (GitSCM)project.getScm(); + git.getExtensions().add(new LocalBranch("**")); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + + assertEquals("GIT_BRANCH", "origin/master", getEnvVars(project).get(GitSCM.GIT_BRANCH)); + assertEquals("GIT_LOCAL_BRANCH", "master", getEnvVars(project).get(GitSCM.GIT_LOCAL_BRANCH)); + } + /** + * Verifies that if project specifies LocalBranch with null value (empty string) + * that the checkout to a local branch using remote branch name sans 'origin'. + * This feature is necessary to support Maven release builds that push updated + * pom.xml to remote branch as + *
        +     * git push origin localbranch:localbranch
        +     * 
        + * @throws Exception on error + */ + @Test + public void testCheckoutToDefaultLocalBranch_NULL() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + GitSCM git = (GitSCM)project.getScm(); + git.getExtensions().add(new LocalBranch("")); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + + assertEquals("GIT_BRANCH", "origin/master", getEnvVars(project).get(GitSCM.GIT_BRANCH)); + assertEquals("GIT_LOCAL_BRANCH", "master", getEnvVars(project).get(GitSCM.GIT_LOCAL_BRANCH)); + } + + /* + * Verifies that GIT_LOCAL_BRANCH is not set if LocalBranch extension + * is not configured. + */ + @Test + public void testCheckoutSansLocalBranchExtension() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + + assertEquals("GIT_BRANCH", "origin/master", getEnvVars(project).get(GitSCM.GIT_BRANCH)); + assertEquals("GIT_LOCAL_BRANCH", null, getEnvVars(project).get(GitSCM.GIT_LOCAL_BRANCH)); + } + + /* + * Verifies that GIT_CHECKOUT_DIR is set to "checkoutDir" if RelativeTargetDirectory extension + * is configured. + */ + @Test + public void testCheckoutRelativeTargetDirectoryExtension() throws Exception { + FreeStyleProject project = setupProject("master", false, "checkoutDir"); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + GitSCM git = (GitSCM)project.getScm(); + git.getExtensions().add(new RelativeTargetDirectory("checkoutDir")); + FreeStyleBuild build1 = build(project, "checkoutDir", Result.SUCCESS, commitFile1); + + assertEquals("GIT_CHECKOUT_DIR", "checkoutDir", getEnvVars(project).get(GitSCM.GIT_CHECKOUT_DIR)); + } + + /* + * Verifies that GIT_CHECKOUT_DIR is not set if RelativeTargetDirectory extension + * is not configured. + */ + @Test + public void testCheckoutSansRelativeTargetDirectoryExtension() throws Exception { + FreeStyleProject project = setupSimpleProject("master"); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + FreeStyleBuild build1 = build(project, Result.SUCCESS, commitFile1); + + assertEquals("GIT_CHECKOUT_DIR", null, getEnvVars(project).get(GitSCM.GIT_CHECKOUT_DIR)); + } + @Test public void testCheckoutFailureIsRetryable() throws Exception { FreeStyleProject project = setupSimpleProject("master"); @@ -1108,13 +2094,18 @@ public void testCheckoutFailureIsRetryable() throws Exception { try { FileUtils.touch(lock); final FreeStyleBuild build2 = build(project, Result.FAILURE); - assertLogContains("java.io.IOException: Could not checkout", build2); + rule.assertLogContains("java.io.IOException: Could not checkout", build2); } finally { lock.delete(); } } + @Test public void testInitSparseCheckout() throws Exception { + if (!sampleRepo.gitVersionAtLeast(1, 7, 10)) { + /* Older git versions have unexpected behaviors with sparse checkout */ + return; + } FreeStyleProject project = setupProject("master", Lists.newArrayList(new SparseCheckoutPath("toto"))); // run build first to create workspace @@ -1130,7 +2121,12 @@ public void testInitSparseCheckout() throws Exception { assertFalse(build1.getWorkspace().child(commitFile2).exists()); } + @Test public void testInitSparseCheckoutBis() throws Exception { + if (!sampleRepo.gitVersionAtLeast(1, 7, 10)) { + /* Older git versions have unexpected behaviors with sparse checkout */ + return; + } FreeStyleProject project = setupProject("master", Lists.newArrayList(new SparseCheckoutPath("titi"))); // run build first to create workspace @@ -1146,7 +2142,12 @@ public void testInitSparseCheckoutBis() throws Exception { assertFalse(build1.getWorkspace().child(commitFile1).exists()); } + @Test public void testSparseCheckoutAfterNormalCheckout() throws Exception { + if (!sampleRepo.gitVersionAtLeast(1, 7, 10)) { + /* Older git versions have unexpected behaviors with sparse checkout */ + return; + } FreeStyleProject project = setupSimpleProject("master"); // run build first to create workspace @@ -1170,7 +2171,12 @@ public void testSparseCheckoutAfterNormalCheckout() throws Exception { assertFalse(build2.getWorkspace().child(commitFile1).exists()); } + @Test public void testNormalCheckoutAfterSparseCheckout() throws Exception { + if (!sampleRepo.gitVersionAtLeast(1, 7, 10)) { + /* Older git versions have unexpected behaviors with sparse checkout */ + return; + } FreeStyleProject project = setupProject("master", Lists.newArrayList(new SparseCheckoutPath("titi"))); // run build first to create workspace @@ -1195,9 +2201,14 @@ public void testNormalCheckoutAfterSparseCheckout() throws Exception { } - public void testInitSparseCheckoutOverSlave() throws Exception { + @Test + public void testInitSparseCheckoutOverAgent() throws Exception { + if (!sampleRepo.gitVersionAtLeast(1, 7, 10)) { + /* Older git versions have unexpected behaviors with sparse checkout */ + return; + } FreeStyleProject project = setupProject("master", Lists.newArrayList(new SparseCheckoutPath("titi"))); - project.setAssignedLabel(createSlave().getSelfLabel()); + project.setAssignedLabel(rule.createSlave().getSelfLabel()); // run build first to create workspace final String commitFile1 = "toto/commitFile1"; @@ -1212,8 +2223,609 @@ public void testInitSparseCheckoutOverSlave() throws Exception { assertFalse(build1.getWorkspace().child(commitFile1).exists()); } + @Test + @Issue("JENKINS-22009") + public void testPolling_environmentValueInBranchSpec() throws Exception { + // create parameterized project with environment value in branch specification + FreeStyleProject project = createFreeStyleProject(); + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("${MY_BRANCH}")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("MY_BRANCH", "master"))); + + // commit something in order to create an initial base version in git + commit("toto/commitFile1", johnDoe, "Commit number 1"); + + // build the project + build(project, Result.SUCCESS); + + assertFalse("No changes to git since last build, thus no new build is expected", project.poll(listener).hasChanges()); + } + + @Issue("JENKINS-29066") + public void baseTestPolling_parentHead(List extensions) throws Exception { + // create parameterized project with environment value in branch specification + FreeStyleProject project = createFreeStyleProject(); + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("**")), + false, Collections.emptyList(), + null, null, + extensions); + project.setScm(scm); + + // commit something in order to create an initial base version in git + commit("toto/commitFile1", johnDoe, "Commit number 1"); + git.branch("someBranch"); + commit("toto/commitFile2", johnDoe, "Commit number 2"); + + assertTrue("polling should detect changes",project.poll(listener).hasChanges()); + + // build the project + build(project, Result.SUCCESS); + + /* Expects 1 build because the build of someBranch incorporates all + * the changes from the master branch as well as the changes from someBranch. + */ + assertEquals("Wrong number of builds", 1, project.getBuilds().size()); + + assertFalse("polling should not detect changes",project.poll(listener).hasChanges()); + } + + @Issue("JENKINS-29066") + @Test + public void testPolling_parentHead() throws Exception { + baseTestPolling_parentHead(Collections.emptyList()); + } + + @Issue("JENKINS-29066") + @Test + public void testPolling_parentHead_DisableRemotePoll() throws Exception { + baseTestPolling_parentHead(Collections.singletonList(new DisableRemotePoll())); + } + + @Test + public void testPollingAfterManualBuildWithParametrizedBranchSpec() throws Exception { + // create parameterized project with environment value in branch specification + FreeStyleProject project = createFreeStyleProject(); + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("${MY_BRANCH}")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("MY_BRANCH", "trackedbranch"))); + + // Initial commit to master + commit("file1", johnDoe, "Initial Commit"); + + // Create the branches + git.branch("trackedbranch"); + git.branch("manualbranch"); + + final StringParameterValue branchParam = new StringParameterValue("MY_BRANCH", "manualbranch"); + final Action[] actions = {new ParametersAction(branchParam)}; + FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserCause(), actions).get(); + rule.assertBuildStatus(Result.SUCCESS, build); + + assertFalse("No changes to git since last build", project.poll(listener).hasChanges()); + + git.checkout("manualbranch"); + commit("file2", johnDoe, "Commit to manually build branch"); + assertFalse("No changes to tracked branch", project.poll(listener).hasChanges()); + + git.checkout("trackedbranch"); + commit("file3", johnDoe, "Commit to tracked branch"); + assertTrue("A change should be detected in tracked branch", project.poll(listener).hasChanges()); + + } + + private final class FakeParametersAction implements EnvironmentContributingAction, Serializable { + // Test class for testPolling_environmentValueAsEnvironmentContributingAction test case + final ParametersAction m_forwardingAction; + + public FakeParametersAction(StringParameterValue params) { + this.m_forwardingAction = new ParametersAction(params); + } + + public void buildEnvVars(AbstractBuild ab, EnvVars ev) { + this.m_forwardingAction.buildEnvVars(ab, ev); + } + + public String getIconFileName() { + return this.m_forwardingAction.getIconFileName(); + } + + public String getDisplayName() { + return this.m_forwardingAction.getDisplayName(); + } + + public String getUrlName() { + return this.m_forwardingAction.getUrlName(); + } + + public List getParameters() { + return this.m_forwardingAction.getParameters(); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + } + + private void readObjectNoData() throws ObjectStreamException { + } + } + + @Test + public void testPolling_CanDoRemotePollingIfOneBranchButMultipleRepositories() throws Exception { + FreeStyleProject project = createFreeStyleProject(); + List remoteConfigs = new ArrayList<>(); + remoteConfigs.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", "", null)); + remoteConfigs.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "someOtherRepo", "", null)); + GitSCM scm = new GitSCM(remoteConfigs, + Collections.singletonList(new BranchSpec("origin/master")), false, + Collections. emptyList(), null, null, + Collections. emptyList()); + project.setScm(scm); + commit("commitFile1", johnDoe, "Commit number 1"); + + FreeStyleBuild first_build = project.scheduleBuild2(0, new Cause.UserCause()).get(); + rule.assertBuildStatus(Result.SUCCESS, first_build); + + first_build.getWorkspace().deleteContents(); + PollingResult pollingResult = scm.poll(project, null, first_build.getWorkspace(), listener, null); + assertFalse(pollingResult.hasChanges()); + } + + @Issue("JENKINS-24467") + @Test + public void testPolling_environmentValueAsEnvironmentContributingAction() throws Exception { + // create parameterized project with environment value in branch specification + FreeStyleProject project = createFreeStyleProject(); + GitSCM scm = new GitSCM( + createRemoteRepositories(), + Collections.singletonList(new BranchSpec("${MY_BRANCH}")), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(scm); + + // Initial commit and build + commit("toto/commitFile1", johnDoe, "Commit number 1"); + String brokenPath = "\\broken/path\\of/doom"; + if (!sampleRepo.gitVersionAtLeast(1, 8)) { + /* Git 1.7.10.4 fails the first build unless the git-upload-pack + * program is available in its PATH. + * Later versions of git don't have that problem. + */ + final String systemPath = System.getenv("PATH"); + brokenPath = systemPath + File.pathSeparator + brokenPath; + } + final StringParameterValue real_param = new StringParameterValue("MY_BRANCH", "master"); + final StringParameterValue fake_param = new StringParameterValue("PATH", brokenPath); + + final Action[] actions = {new ParametersAction(real_param), new FakeParametersAction(fake_param)}; + + // SECURITY-170 - have to use ParametersDefinitionProperty + project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("MY_BRANCH", "master"))); + + FreeStyleBuild first_build = project.scheduleBuild2(0, new Cause.UserCause(), actions).get(); + rule.assertBuildStatus(Result.SUCCESS, first_build); + + Launcher launcher = workspace.createLauncher(listener); + final EnvVars environment = GitUtils.getPollEnvironment(project, workspace, launcher, listener); + + assertEquals(environment.get("MY_BRANCH"), "master"); + assertNotSame("Environment path should not be broken path", environment.get("PATH"), brokenPath); + } + + /** + * Tests that builds have the correctly specified Custom SCM names, associated with each build. + * @throws Exception on error + */ + @Ignore("Intermittent failures on stable-3.10 branch and master branch, not on stable-3.9") + @Test + public void testCustomSCMName() throws Exception { + final String branchName = "master"; + final FreeStyleProject project = setupProject(branchName, false); + project.addTrigger(new SCMTrigger("")); + GitSCM git = (GitSCM) project.getScm(); + setupJGit(git); + + final String commitFile1 = "commitFile1"; + final String scmNameString1 = ""; + commit(commitFile1, johnDoe, "Commit number 1"); + assertTrue("scm polling should not detect any more changes after build", + project.poll(listener).hasChanges()); + build(project, Result.SUCCESS, commitFile1); + final ObjectId commit1 = testRepo.git.revListAll().get(0); + + // Check unset build SCM Name carries + final int buildNumber1 = notifyAndCheckScmName( + project, commit1, scmNameString1, 1, git); + + final String scmNameString2 = "ScmName2"; + git.getExtensions().replace(new ScmName(scmNameString2)); + + commit("commitFile2", johnDoe, "Commit number 2"); + assertTrue("scm polling should detect commit 2 (commit1=" + commit1 + ")", project.poll(listener).hasChanges()); + final ObjectId commit2 = testRepo.git.revListAll().get(0); + + // Check second set SCM Name + final int buildNumber2 = notifyAndCheckScmName( + project, commit2, scmNameString2, 2, git, commit1); + checkNumberedBuildScmName(project, buildNumber1, scmNameString1, git); + + final String scmNameString3 = "ScmName3"; + git.getExtensions().replace(new ScmName(scmNameString3)); + + commit("commitFile3", johnDoe, "Commit number 3"); + assertTrue("scm polling should detect commit 3, (commit2=" + commit2 + ",commit1=" + commit1 + ")", project.poll(listener).hasChanges()); + final ObjectId commit3 = testRepo.git.revListAll().get(0); + + // Check third set SCM Name + final int buildNumber3 = notifyAndCheckScmName( + project, commit3, scmNameString3, 3, git, commit2, commit1); + checkNumberedBuildScmName(project, buildNumber1, scmNameString1, git); + checkNumberedBuildScmName(project, buildNumber2, scmNameString2, git); + + commit("commitFile4", johnDoe, "Commit number 4"); + assertTrue("scm polling should detect commit 4 (commit3=" + commit3 + ",commit2=" + commit2 + ",commit1=" + commit1 + ")", project.poll(listener).hasChanges()); + final ObjectId commit4 = testRepo.git.revListAll().get(0); + + // Check third set SCM Name still set + final int buildNumber4 = notifyAndCheckScmName( + project, commit4, scmNameString3, 4, git, commit3, commit2, commit1); + checkNumberedBuildScmName(project, buildNumber1, scmNameString1, git); + checkNumberedBuildScmName(project, buildNumber2, scmNameString2, git); + checkNumberedBuildScmName(project, buildNumber3, scmNameString3, git); + } + + /** + * Method performs HTTP get on "notifyCommit" URL, passing it commit by SHA1 + * and tests for custom SCM name build data consistency. + * @param project project to build + * @param commit commit to build + * @param expectedScmName Expected SCM name for commit. + * @param ordinal number of commit to log into errors, if any + * @param git git SCM + * @throws Exception on error + */ + private int notifyAndCheckScmName(FreeStyleProject project, ObjectId commit, + String expectedScmName, int ordinal, GitSCM git, ObjectId... priorCommits) throws Exception { + String priorCommitIDs = ""; + for (ObjectId priorCommit : priorCommits) { + priorCommitIDs = priorCommitIDs + " " + priorCommit; + } + assertTrue("scm polling should detect commit " + ordinal, notifyCommit(project, commit)); + + final Build build = project.getLastBuild(); + final BuildData buildData = git.getBuildData(build); + assertEquals("Expected SHA1 != built SHA1 for commit " + ordinal + " priors:" + priorCommitIDs, commit, buildData + .getLastBuiltRevision().getSha1()); + assertEquals("Expected SHA1 != retrieved SHA1 for commit " + ordinal + " priors:" + priorCommitIDs, commit, buildData.getLastBuild(commit).getSHA1()); + assertTrue("Commit " + ordinal + " not marked as built", buildData.hasBeenBuilt(commit)); + + assertEquals("Wrong SCM Name for commit " + ordinal, expectedScmName, buildData.getScmName()); + + return build.getNumber(); + } + + private void checkNumberedBuildScmName(FreeStyleProject project, int buildNumber, + String expectedScmName, GitSCM git) throws Exception { + + final BuildData buildData = git.getBuildData(project.getBuildByNumber(buildNumber)); + assertEquals("Wrong SCM Name", expectedScmName, buildData.getScmName()); + } + + /* + * Tests that builds have the correctly specified branches, associated with + * the commit id, passed with "notifyCommit" URL. + */ + @Ignore("Intermittent failures on stable-3.10 branch, not on stable-3.9 or master") + @Issue("JENKINS-24133") + // Flaky test distracting from primary focus + // @Test + public void testSha1NotificationBranches() throws Exception { + final String branchName = "master"; + final FreeStyleProject project = setupProject(branchName, false); + project.addTrigger(new SCMTrigger("")); + final GitSCM git = (GitSCM) project.getScm(); + setupJGit(git); + + final String commitFile1 = "commitFile1"; + commit(commitFile1, johnDoe, "Commit number 1"); + assertTrue("scm polling should detect commit 1", + project.poll(listener).hasChanges()); + build(project, Result.SUCCESS, commitFile1); + final ObjectId commit1 = testRepo.git.revListAll().get(0); + notifyAndCheckBranch(project, commit1, branchName, 1, git); + + commit("commitFile2", johnDoe, "Commit number 2"); + assertTrue("scm polling should detect commit 2", project.poll(listener).hasChanges()); + final ObjectId commit2 = testRepo.git.revListAll().get(0); + notifyAndCheckBranch(project, commit2, branchName, 2, git); + + notifyAndCheckBranch(project, commit1, branchName, 1, git); + } + + /* A null pointer exception was detected because the plugin failed to + * write a branch name to the build data, so there was a SHA1 recorded + * in the build data, but no branch name. + */ + @Test + public void testNoNullPointerExceptionWithNullBranch() throws Exception { + ObjectId sha1 = ObjectId.fromString("2cec153f34767f7638378735dc2b907ed251a67d"); + + /* This is the null that causes NPE */ + Branch branch = new Branch(null, sha1); + + List branchList = new ArrayList<>(); + branchList.add(branch); + + Revision revision = new Revision(sha1, branchList); + + /* BuildData mock that will use the Revision with null branch name */ + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + Mockito.when(buildData.hasBeenReferenced(anyString())).thenReturn(true); + + /* List of build data that will be returned by the mocked BuildData */ + List buildDataList = new ArrayList<>(); + buildDataList.add(buildData); + + /* AbstractBuild mock which returns the buildDataList that contains a null branch name */ + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getActions(BuildData.class)).thenReturn(buildDataList); + + final FreeStyleProject project = setupProject("*/*", false); + GitSCM scm = (GitSCM) project.getScm(); + scm.buildEnvVars(build, new EnvVars()); // NPE here before fix applied + + /* Verify mocks were called as expected */ + verify(buildData, times(1)).getLastBuiltRevision(); + verify(buildData, times(1)).hasBeenReferenced(anyString()); + verify(build, times(1)).getActions(BuildData.class); + } + + @Test + public void testBuildEnvVarsLocalBranchStarStar() throws Exception { + ObjectId sha1 = ObjectId.fromString("2cec153f34767f7638378735dc2b907ed251a67d"); + + /* This is the null that causes NPE */ + Branch branch = new Branch("origin/master", sha1); + + List branchList = new ArrayList<>(); + branchList.add(branch); + + Revision revision = new Revision(sha1, branchList); + + /* BuildData mock that will use the Revision with null branch name */ + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + Mockito.when(buildData.hasBeenReferenced(anyString())).thenReturn(true); + + /* List of build data that will be returned by the mocked BuildData */ + List buildDataList = new ArrayList<>(); + buildDataList.add(buildData); + + /* AbstractBuild mock which returns the buildDataList that contains a null branch name */ + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getActions(BuildData.class)).thenReturn(buildDataList); + + final FreeStyleProject project = setupProject("*/*", false); + GitSCM scm = (GitSCM) project.getScm(); + scm.getExtensions().add(new LocalBranch("**")); + + EnvVars env = new EnvVars(); + scm.buildEnvVars(build, env); // NPE here before fix applied + + assertEquals("GIT_BRANCH", "origin/master", env.get("GIT_BRANCH")); + assertEquals("GIT_LOCAL_BRANCH", "master", env.get("GIT_LOCAL_BRANCH")); + + /* Verify mocks were called as expected */ + verify(buildData, times(1)).getLastBuiltRevision(); + verify(buildData, times(1)).hasBeenReferenced(anyString()); + verify(build, times(1)).getActions(BuildData.class); + } + + @Test + public void testBuildEnvVarsLocalBranchNull() throws Exception { + ObjectId sha1 = ObjectId.fromString("2cec153f34767f7638378735dc2b907ed251a67d"); + + /* This is the null that causes NPE */ + Branch branch = new Branch("origin/master", sha1); + + List branchList = new ArrayList<>(); + branchList.add(branch); + + Revision revision = new Revision(sha1, branchList); + + /* BuildData mock that will use the Revision with null branch name */ + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + Mockito.when(buildData.hasBeenReferenced(anyString())).thenReturn(true); + + /* List of build data that will be returned by the mocked BuildData */ + List buildDataList = new ArrayList<>(); + buildDataList.add(buildData); + + /* AbstractBuild mock which returns the buildDataList that contains a null branch name */ + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getActions(BuildData.class)).thenReturn(buildDataList); + + final FreeStyleProject project = setupProject("*/*", false); + GitSCM scm = (GitSCM) project.getScm(); + scm.getExtensions().add(new LocalBranch("")); + + EnvVars env = new EnvVars(); + scm.buildEnvVars(build, env); // NPE here before fix applied + + assertEquals("GIT_BRANCH", "origin/master", env.get("GIT_BRANCH")); + assertEquals("GIT_LOCAL_BRANCH", "master", env.get("GIT_LOCAL_BRANCH")); + + /* Verify mocks were called as expected */ + verify(buildData, times(1)).getLastBuiltRevision(); + verify(buildData, times(1)).hasBeenReferenced(anyString()); + verify(build, times(1)).getActions(BuildData.class); + } + + @Test + public void testBuildEnvVarsLocalBranchNotSet() throws Exception { + ObjectId sha1 = ObjectId.fromString("2cec153f34767f7638378735dc2b907ed251a67d"); + + /* This is the null that causes NPE */ + Branch branch = new Branch("origin/master", sha1); + + List branchList = new ArrayList<>(); + branchList.add(branch); + + Revision revision = new Revision(sha1, branchList); + + /* BuildData mock that will use the Revision with null branch name */ + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.getLastBuiltRevision()).thenReturn(revision); + Mockito.when(buildData.hasBeenReferenced(anyString())).thenReturn(true); + + /* List of build data that will be returned by the mocked BuildData */ + List buildDataList = new ArrayList<>(); + buildDataList.add(buildData); + + /* AbstractBuild mock which returns the buildDataList that contains a null branch name */ + AbstractBuild build = Mockito.mock(AbstractBuild.class); + Mockito.when(build.getActions(BuildData.class)).thenReturn(buildDataList); + + final FreeStyleProject project = setupProject("*/*", false); + GitSCM scm = (GitSCM) project.getScm(); + + EnvVars env = new EnvVars(); + scm.buildEnvVars(build, env); // NPE here before fix applied + + assertEquals("GIT_BRANCH", "origin/master", env.get("GIT_BRANCH")); + assertEquals("GIT_LOCAL_BRANCH", null, env.get("GIT_LOCAL_BRANCH")); + + /* Verify mocks were called as expected */ + verify(buildData, times(1)).getLastBuiltRevision(); + verify(buildData, times(1)).hasBeenReferenced(anyString()); + verify(build, times(1)).getActions(BuildData.class); + } + + @Issue("JENKINS-38241") + @Test + public void testCommitMessageIsPrintedToLogs() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=test commit"); + FreeStyleProject p = setupSimpleProject("master"); + Run run = rule.buildAndAssertSuccess(p); + TaskListener mockListener = Mockito.mock(TaskListener.class); + Mockito.when(mockListener.getLogger()).thenReturn(Mockito.spy(StreamTaskListener.fromStdout().getLogger())); + + p.getScm().checkout(run, new Launcher.LocalLauncher(listener), + new FilePath(run.getRootDir()).child("tmp-" + "master"), + mockListener, null, SCMRevisionState.NONE); + + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(String.class); + verify(mockListener.getLogger(), atLeastOnce()).println(logCaptor.capture()); + List values = logCaptor.getAllValues(); + assertThat(values, hasItem("Commit message: \"test commit\"")); + } + + /** + * Method performs HTTP get on "notifyCommit" URL, passing it commit by SHA1 + * and tests for build data consistency. + * @param project project to build + * @param commit commit to build + * @param expectedBranch branch, that is expected to be built + * @param ordinal number of commit to log into errors, if any + * @param git git SCM + * @throws Exception on error + */ + private void notifyAndCheckBranch(FreeStyleProject project, ObjectId commit, + String expectedBranch, int ordinal, GitSCM git) throws Exception { + assertTrue("scm polling should detect commit " + ordinal, notifyCommit(project, commit)); + final BuildData buildData = git.getBuildData(project.getLastBuild()); + final Collection builtBranches = buildData.lastBuild.getRevision().getBranches(); + assertEquals("Commit " + ordinal + " should be built", commit, buildData + .getLastBuiltRevision().getSha1()); + + final String expectedBranchString = "origin/" + expectedBranch; + assertFalse("Branches should be detected for the build", builtBranches.isEmpty()); + assertEquals(expectedBranch + " branch should be detected", expectedBranchString, + builtBranches.iterator().next().getName()); + assertEquals(expectedBranchString, getEnvVars(project).get(GitSCM.GIT_BRANCH)); + } + + /** + * Method performs commit notification for the last committed SHA1 using + * notifyCommit URL. + * @param project project to trigger + * @return whether the new build has been triggered (true) or + * not (false). + * @throws Exception on error + */ + private boolean notifyCommit(FreeStyleProject project, ObjectId commitId) throws Exception { + final int initialBuildNumber = project.getLastBuild().getNumber(); + final String commit1 = ObjectId.toString(commitId); + + final String notificationPath = rule.getURL().toExternalForm() + + "git/notifyCommit?url=" + testRepo.gitDir.toString() + "&sha1=" + commit1; + final URL notifyUrl = new URL(notificationPath); + String notifyContent = null; + try (final InputStream is = notifyUrl.openStream()) { + notifyContent = IOUtils.toString(is); + } + assertThat(notifyContent, containsString("No Git consumers using SCM API plugin for: " + testRepo.gitDir.toString())); + + if ((project.getLastBuild().getNumber() == initialBuildNumber) + && (rule.jenkins.getQueue().isEmpty())) { + return false; + } else { + while (!rule.jenkins.getQueue().isEmpty()) { + Thread.sleep(100); + } + final FreeStyleBuild build = project.getLastBuild(); + while (build.isBuilding()) { + Thread.sleep(100); + } + return true; + } + } + private void setupJGit(GitSCM git) { git.gitTool="jgit"; - jenkins.getDescriptorByType(GitTool.DescriptorImpl.class).setInstallations(new JGitTool(Collections.>emptyList())); + rule.jenkins.getDescriptorByType(GitTool.DescriptorImpl.class).setInstallations(new JGitTool(Collections.>emptyList())); + } + + /** We clean the environment, just in case the test is being run from a Jenkins job using this same plugin :). */ + @TestExtension + public static class CleanEnvironment extends EnvironmentContributor { + @Override + public void buildEnvironmentFor(Run run, EnvVars envs, TaskListener listener) { + envs.remove(GitSCM.GIT_BRANCH); + envs.remove(GitSCM.GIT_LOCAL_BRANCH); + envs.remove(GitSCM.GIT_COMMIT); + envs.remove(GitSCM.GIT_PREVIOUS_COMMIT); + envs.remove(GitSCM.GIT_PREVIOUS_SUCCESSFUL_COMMIT); + } + } + + /** Returns true if test cleanup is not reliable */ + private boolean cleanupIsUnreliable() { + // Windows cleanup is unreliable on ci.jenkins.io + String jobUrl = System.getenv("JOB_URL"); + return isWindows() && jobUrl != null && jobUrl.contains("ci.jenkins.io"); + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return java.io.File.pathSeparatorChar==';'; } } diff --git a/src/test/java/hudson/plugins/git/GitSCMUnitTest.java b/src/test/java/hudson/plugins/git/GitSCMUnitTest.java new file mode 100644 index 0000000000..1d68d2fdf8 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitSCMUnitTest.java @@ -0,0 +1,327 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import hudson.EnvVars; +import static hudson.plugins.git.GitSCM.createRepoList; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.browser.GithubWeb; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.opt.PreBuildMergeOptions; +import hudson.plugins.git.util.AncestryBuildChooser; +import hudson.plugins.git.util.BuildChooser; +import hudson.plugins.git.util.DefaultBuildChooser; +import hudson.scm.RepositoryBrowser; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; + +public class GitSCMUnitTest { + + private final String gitDir = "."; + private final GitSCM gitSCM = new GitSCM(gitDir); + private final String repoURL = "https://github.com/jenkinsci/git-plugin"; + + public GitSCMUnitTest() { + } + + @Test + public void testGetSubmoduleCfg() { + Collection emptySubmoduleConfigList = new ArrayList<>(); + assertThat(gitSCM.getSubmoduleCfg(), is(emptySubmoduleConfigList)); + } + + @Test + public void testSetSubmoduleCfg() { + Collection submoduleConfigList = new ArrayList<>(); + SubmoduleConfig config = new SubmoduleConfig(); + submoduleConfigList.add(config); + gitSCM.setSubmoduleCfg(submoduleConfigList); + assertThat(gitSCM.getSubmoduleCfg(), is(submoduleConfigList)); + } + + @Test + public void testCreateRepoList() { + String name = null; + String refspec = null; + String credentialsId = null; + List expectedRemoteConfigList = new ArrayList<>(); + UserRemoteConfig remoteConfig = new UserRemoteConfig(repoURL, name, refspec, credentialsId); + expectedRemoteConfigList.add(remoteConfig); + List remoteConfigList = GitSCM.createRepoList(repoURL, credentialsId); + assertUserRemoteConfigListEquals(remoteConfigList, expectedRemoteConfigList); + } + + private void assertUserRemoteConfigListEquals(List remoteConfigList, List expectedRemoteConfigList) { + /* UserRemoteConfig lacks an equals method - ugh */ + assertThat(remoteConfigList.toString(), is(expectedRemoteConfigList.toString())); + assertThat(remoteConfigList.get(0).getUrl(), is(expectedRemoteConfigList.get(0).getUrl())); + assertThat(remoteConfigList.get(0).getName(), is(expectedRemoteConfigList.get(0).getName())); + assertThat(remoteConfigList.get(0).getRefspec(), is(expectedRemoteConfigList.get(0).getRefspec())); + assertThat(remoteConfigList.get(0).getCredentialsId(), is(expectedRemoteConfigList.get(0).getCredentialsId())); + assertThat(remoteConfigList.size(), is(1)); + } + + @Test + public void testGetBrowser() { + assertThat(gitSCM.getBrowser(), is(nullValue())); + } + + @Test + public void testSetBrowser() { + GitRepositoryBrowser browser = new GithubWeb(repoURL); + gitSCM.setBrowser(browser); + assertThat(gitSCM.getBrowser(), is(browser)); + } + + @Test + public void testGuessBrowser() { + /* Well tested in other classes */ + RepositoryBrowser result = gitSCM.guessBrowser(); + assertThat(result, is(nullValue())); + } + + @Test + public void testGetBuildChooser() { + assertThat(gitSCM.getBuildChooser(), is(instanceOf(DefaultBuildChooser.class))); + } + + @Test + public void testSetBuildChooser() throws Exception { + BuildChooser ancestryBuildChooser = new AncestryBuildChooser(1, "string"); + gitSCM.setBuildChooser(ancestryBuildChooser); + assertThat(gitSCM.getBuildChooser(), is(ancestryBuildChooser)); + } + + @Test + public void testSetBuildChooserDefault() throws Exception { + BuildChooser ancestryBuildChooser = new AncestryBuildChooser(1, "string"); + gitSCM.setBuildChooser(ancestryBuildChooser); + BuildChooser defaultBuildChooser = new DefaultBuildChooser(); + gitSCM.setBuildChooser(defaultBuildChooser); + assertThat(gitSCM.getBuildChooser(), is(instanceOf(DefaultBuildChooser.class))); + } + + @Test + public void testGetRepositoryByName() throws Exception { + RemoteConfig expected = new RemoteConfig(new Config(), "origin"); + expected.addURI(new URIish(gitDir)); + assertRemoteConfigEquals(gitSCM.getRepositoryByName("origin"), expected); + } + + private void assertRemoteConfigEquals(RemoteConfig remoteConfig, RemoteConfig expected) { + assertThat(remoteConfig.getName(), is(expected.getName())); + assertThat(remoteConfig.getURIs(), is(expected.getURIs())); + } + + private void assertRemoteConfigListEquals(List remoteConfigList, List expectedList) { + int expectedIndex = 0; + for (RemoteConfig remoteConfig : remoteConfigList) { + assertRemoteConfigEquals(remoteConfig, expectedList.get(expectedIndex++)); + } + } + + @Test + public void testGetRepositoryByNameNoSuchName() { + assertThat(gitSCM.getRepositoryByName("no-such-name"), is(nullValue())); + } + + @Test + public void testGetRepositoryByNameEmptyName() { + assertThat(gitSCM.getRepositoryByName(""), is(nullValue())); + } + + @Test + public void testGetRepositoryByNameNullName() { + assertThat(gitSCM.getRepositoryByName(null), is(nullValue())); + } + + @Test + public void testGetUserRemoteConfigs() { + String name = null; + String refspec = null; + String credentialsId = null; + List expectedRemoteConfigList = new ArrayList<>(); + UserRemoteConfig remoteConfig = new UserRemoteConfig(gitDir, name, refspec, credentialsId); + expectedRemoteConfigList.add(remoteConfig); + assertUserRemoteConfigListEquals(gitSCM.getUserRemoteConfigs(), expectedRemoteConfigList); + } + + @Test + public void testGetRepositories() throws Exception { + List expectedRemoteConfigList = new ArrayList<>(); + RemoteConfig remoteConfig = new RemoteConfig(new Config(), "origin"); + remoteConfig.addURI(new URIish(gitDir)); + expectedRemoteConfigList.add(remoteConfig); + assertRemoteConfigListEquals(gitSCM.getRepositories(), expectedRemoteConfigList); + } + + @Test + public void testDeriveLocalBranchName() { + assertThat(gitSCM.deriveLocalBranchName("origin/master"), is("master")); + assertThat(gitSCM.deriveLocalBranchName("master"), is("master")); + assertThat(gitSCM.deriveLocalBranchName("origin/feature/xyzzy"), is("feature/xyzzy")); + assertThat(gitSCM.deriveLocalBranchName("feature/xyzzy"), is("feature/xyzzy")); + } + + @Test + public void testGetGitTool() { + assertThat(gitSCM.getGitTool(), is(nullValue())); + } + + @Test + public void testGetParameterString() { + String original = "${A}/${B} ${A}/${C}"; + EnvVars env = new EnvVars(); + env.put("A", "A-value"); + env.put("B", "B-value"); + assertThat(GitSCM.getParameterString(original, env), is("A-value/B-value A-value/${C}")); + } + + @Test + public void testRequiresWorkspaceForPolling() { + /* Assumes workspace is required */ + assertTrue(gitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingSingleBranch() { + /* Force single-branch use case */ + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("master")), + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertFalse(bigGitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingSingleBranchWithRemoteName() { + /* Force single-branch use case */ + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("origin/master")), + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertFalse(bigGitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingSingleBranchWithWildcardRemoteName() { + /* Force single-branch use case */ + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("*/master")), + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertFalse(bigGitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingSingleBranchWithWildcardSuffix() { + /* Force single-branch use case */ + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("master*")), + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertTrue(bigGitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingMultiBranch() { + /* Multi-branch use case */ + List branches = new ArrayList<>(); + branches.add(new BranchSpec("master")); + branches.add(new BranchSpec("origin/master")); + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + branches, + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertTrue(bigGitSCM.requiresWorkspaceForPolling()); + } + + @Test + public void testRequiresWorkspaceForPollingEmptyBranchName() { + /* Multi-branch use case */ + EnvVars env = new EnvVars(); + env.put("A", ""); + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("${A}")), + false, Collections.emptyList(), + null, null, Collections.emptyList()); + assertFalse(bigGitSCM.requiresWorkspaceForPolling(env)); + } + + @Test + public void testCreateChangeLogParser() { + assertThat(gitSCM.createChangeLogParser(), is(instanceOf(GitChangeLogParser.class))); + } + + @Test + public void testIsDoGenerateSubmoduleConfigurations() { + assertFalse(gitSCM.isDoGenerateSubmoduleConfigurations()); + } + + @Test + public void testIsDoGenerateSubmoduleConfigurationsTrue() { + GitSCM bigGitSCM = new GitSCM(createRepoList(repoURL, null), + Collections.singletonList(new BranchSpec("master")), + true, Collections.emptyList(), + null, null, Collections.emptyList()); + assertTrue(bigGitSCM.isDoGenerateSubmoduleConfigurations()); + } + + @Test + public void testGetBranches() { + List expectedBranchList = new ArrayList<>(); + expectedBranchList.add(new BranchSpec("**")); + assertBranchSpecListEquals(gitSCM.getBranches(), expectedBranchList); + } + + private void assertBranchSpecListEquals(List branchList, List expectedBranchList) { + int expectedIndex = 0; + for (BranchSpec branchSpec : branchList) { + assertThat(branchSpec.getName(), is(expectedBranchList.get(expectedIndex++).getName())); + } + assertThat(branchList.size(), is(expectedBranchList.size())); + } + + @Test + public void testGetKey() { + assertThat(gitSCM.getKey(), is("git " + gitDir)); + } + + @Test + @Deprecated + public void testGetMergeOptions() throws Exception { + PreBuildMergeOptions expectedMergeOptions = new PreBuildMergeOptions(); + PreBuildMergeOptions mergeOptions = gitSCM.getMergeOptions(); + assertThat(mergeOptions.getRemoteBranchName(), is(expectedMergeOptions.getRemoteBranchName())); + assertThat(mergeOptions.getMergeTarget(), is(expectedMergeOptions.getMergeTarget())); + } +} diff --git a/src/test/java/hudson/plugins/git/GitStatusCrumbExclusionTest.java b/src/test/java/hudson/plugins/git/GitStatusCrumbExclusionTest.java new file mode 100644 index 0000000000..2b8c1f1cb2 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitStatusCrumbExclusionTest.java @@ -0,0 +1,60 @@ +package hudson.plugins.git; + +import hudson.security.csrf.CrumbFilter; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.jvnet.hudson.test.JenkinsRule; + +public class GitStatusCrumbExclusionTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + private CrumbFilter filter; + private HttpServletRequest req; + private HttpServletResponse resp; + private FilterChain chain; + + @Before + public void before() { + filter = new CrumbFilter(); + req = mock(HttpServletRequest.class); + resp = mock(HttpServletResponse.class); + chain = mock(FilterChain.class); + } + + @Test + public void testNotifyCommit() throws Exception { + when(req.getPathInfo()).thenReturn("/git/notifyCommit"); + when(req.getMethod()).thenReturn("POST"); + when(req.getParameterNames()).thenReturn(Collections.emptyEnumeration()); + filter.doFilter(req, resp, chain); + verify(resp, never()).sendError(anyInt(), anyString()); + } + + @Test + public void testInvalidPath() throws Exception { + when(req.getPathInfo()).thenReturn("/git/somethingElse"); + when(req.getMethod()).thenReturn("POST"); + when(req.getParameterNames()).thenReturn(Collections.emptyEnumeration()); + filter.doFilter(req, resp, chain); + verify(resp, times(1)).sendError(anyInt(), anyString()); + } +} diff --git a/src/test/java/hudson/plugins/git/GitStatusMultipleSCMTest.java b/src/test/java/hudson/plugins/git/GitStatusMultipleSCMTest.java new file mode 100644 index 0000000000..4e70040ba5 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitStatusMultipleSCMTest.java @@ -0,0 +1,119 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jenkinsci.plugins.multiplescms.MultiSCM; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.FreeStyleProject; +import hudson.model.TaskListener; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.scm.SCM; +import hudson.triggers.SCMTrigger; +import hudson.util.StreamTaskListener; + +import javax.servlet.http.HttpServletRequest; + +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class GitStatusMultipleSCMTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + private GitStatus gitStatus; + private TestGitRepo repo0; + private TestGitRepo repo1; + private FreeStyleProject testProject; + + @Before + public void setUp() throws Exception { + gitStatus = new GitStatus(); + TaskListener listener = StreamTaskListener.fromStderr(); + repo0 = new TestGitRepo("repo0", tmp.newFolder(), listener); + repo1 = new TestGitRepo("repo1", tmp.newFolder(), listener); + testProject = r.createFreeStyleProject(); + } + + @Test + @Issue("JENKINS-26587") + public void commitNotificationIsPropagatedOnlyToSourceRepository() throws Exception { + setupProject("master", false); + + repo0.commit("repo0", repo1.johnDoe, "repo0 commit 1"); + final String repo1sha1 = repo1.commit("repo1", repo1.janeDoe, "repo1 commit 1"); + + this.gitStatus.doNotifyCommit(requestWithoutParameters(), repo1.remoteConfigs().get(0).getUrl(), null, repo1sha1); + assertTrue("expected a build start on notify", r.isSomethingHappening()); + + r.waitUntilNoActivity(); + + final List projects = r.getInstance().getAllItems(AbstractProject.class); + assertThat("should contain previously created project", projects.size(), greaterThan(0)); + + for (AbstractProject project : projects) { + final AbstractBuild lastBuild = project.getLastBuild(); + assertNotNull("one build should've been built after notification", lastBuild); + r.assertBuildStatusSuccess(lastBuild); + } + } + + private HttpServletRequest requestWithoutParameters() { + return mock(HttpServletRequest.class); + } + + private SCMTrigger setupProject(String branchString, boolean ignoreNotifyCommit) throws Exception { + SCMTrigger trigger = new SCMTrigger("", ignoreNotifyCommit); + setupProject(branchString, trigger); + return trigger; + } + + private void setupProject(String branchString, SCMTrigger trigger) throws Exception { + List branch = Collections.singletonList(new BranchSpec(branchString)); + + SCM repo0Scm = new GitSCM( + repo0.remoteConfigs(), + branch, + false, + Collections.emptyList(), + null, + null, + Collections.emptyList()); + + SCM repo1Scm = new GitSCM( + repo1.remoteConfigs(), + branch, + false, + Collections.emptyList(), + null, + null, + Collections.emptyList()); + + List testScms = new ArrayList<>(); + testScms.add(repo0Scm); + testScms.add(repo1Scm); + + MultiSCM scm = new MultiSCM(testScms); + + testProject.setScm(scm); + testProject.getBuildersList().add(new CaptureEnvironmentBuilder()); + + if (trigger != null) { + testProject.addTrigger(trigger); + } + } +} diff --git a/src/test/java/hudson/plugins/git/GitStatusTest.java b/src/test/java/hudson/plugins/git/GitStatusTest.java index fced95e6be..b9bd10bad8 100644 --- a/src/test/java/hudson/plugins/git/GitStatusTest.java +++ b/src/test/java/hudson/plugins/git/GitStatusTest.java @@ -1,136 +1,358 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ package hudson.plugins.git; +import hudson.model.Action; +import hudson.model.Cause; +import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; +import hudson.model.ParameterValue; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.StringParameterDefinition; import hudson.plugins.git.extensions.GitSCMExtension; -import hudson.plugins.git.extensions.impl.IgnoreNotifyCommit; -import hudson.plugins.git.util.DefaultBuildChooser; +import hudson.tasks.BatchFile; +import hudson.tasks.CommandInterpreter; +import hudson.tasks.Shell; import hudson.triggers.SCMTrigger; - +import java.io.File; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.util.*; import org.eclipse.jgit.transport.URIish; +import org.mockito.Mockito; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.jvnet.hudson.test.WithoutJenkins; -import org.jvnet.hudson.test.HudsonTestCase; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import javax.servlet.http.HttpServletRequest; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +@RunWith(Theories.class) +public class GitStatusTest extends AbstractGitProject { -public class GitStatusTest extends HudsonTestCase { private GitStatus gitStatus; + private HttpServletRequest requestWithNoParameter; + private HttpServletRequest requestWithParameter; + private String repoURL; + private String branch; + private String sha1; - public GitStatusTest(String testName) { - super(testName); + @Before + public void setUp() throws Exception { + GitStatus.setAllowNotifyCommitParameters(false); + GitStatus.setSafeParametersForTest(null); + this.gitStatus = new GitStatus(); + this.requestWithNoParameter = mock(HttpServletRequest.class); + this.requestWithParameter = mock(HttpServletRequest.class); + this.repoURL = new File(".").getAbsolutePath(); + this.branch = "**"; + this.sha1 = "7bb68ef21dc90bd4f7b08eca876203b2e049198d"; } - @Override - protected void setUp() throws Exception { - super.setUp(); - this.gitStatus = new GitStatus(); + @After + public void resetAllowNotifyCommitParameters() throws Exception { + GitStatus.setAllowNotifyCommitParameters(false); + GitStatus.setSafeParametersForTest(null); } + @WithoutJenkins + @Test public void testGetDisplayName() { assertEquals("Git", this.gitStatus.getDisplayName()); } - public void testGetSearchUrl() { - assertEquals("git", this.gitStatus.getSearchUrl()); - } - + @WithoutJenkins + @Test public void testGetIconFileName() { assertNull(this.gitStatus.getIconFileName()); } + @WithoutJenkins + @Test public void testGetUrlName() { assertEquals("git", this.gitStatus.getUrlName()); } + @WithoutJenkins + @Test + public void testToString() { + assertEquals("URL: ", this.gitStatus.toString()); + } + + @WithoutJenkins + @Test + public void testAllowNotifyCommitParametersDisabled() { + assertEquals("SECURITY-275: ignore arbitrary notifyCommit parameters", false, GitStatus.ALLOW_NOTIFY_COMMIT_PARAMETERS); + } + + @WithoutJenkins + @Test + public void testSafeParametersEmpty() { + assertEquals("SECURITY-275: Safe notifyCommit parameters", "", GitStatus.SAFE_PARAMETERS); + } + + @Test public void testDoNotifyCommitWithNoBranches() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", false); - SCMTrigger aTopicTrigger = setupProject("a", "topic", false); - SCMTrigger bMasterTrigger = setupProject("b", "master", false); - SCMTrigger bTopicTrigger = setupProject("b", "topic", false); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit("a", "", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "", null); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: ", this.gitStatus.toString()); } + @Test public void testDoNotifyCommitWithNoMatchingUrl() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", false); - SCMTrigger aTopicTrigger = setupProject("a", "topic", false); - SCMTrigger bMasterTrigger = setupProject("b", "master", false); - SCMTrigger bTopicTrigger = setupProject("b", "topic", false); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit("nonexistent", "", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "nonexistent", "", null); Mockito.verify(aMasterTrigger, Mockito.never()).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + assertEquals("URL: nonexistent Branches: ", this.gitStatus.toString()); } + @Test public void testDoNotifyCommitWithOneBranch() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", false); - SCMTrigger aTopicTrigger = setupProject("a", "topic", false); - SCMTrigger bMasterTrigger = setupProject("b", "master", false); - SCMTrigger bTopicTrigger = setupProject("b", "topic", false); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit("a", "master", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: master", this.gitStatus.toString()); } + @Test public void testDoNotifyCommitWithTwoBranches() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", false); - SCMTrigger aTopicTrigger = setupProject("a", "topic", false); - SCMTrigger bMasterTrigger = setupProject("b", "master", false); - SCMTrigger bTopicTrigger = setupProject("b", "topic", false); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger aFeatureTrigger = setupProjectWithTrigger("a", "feature/def", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); + SCMTrigger bFeatureTrigger = setupProjectWithTrigger("b", "feature/def", false); - this.gitStatus.doNotifyCommit("a", "master,topic", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master,topic,feature/def", null); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger).run(); + Mockito.verify(aFeatureTrigger).run(); + Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); + Mockito.verify(bFeatureTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: master,topic,feature/def", this.gitStatus.toString()); } + @Test public void testDoNotifyCommitWithNoMatchingBranches() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", false); - SCMTrigger aTopicTrigger = setupProject("a", "topic", false); - SCMTrigger bMasterTrigger = setupProject("b", "master", false); - SCMTrigger bTopicTrigger = setupProject("b", "topic", false); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit("a", "nonexistent", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "nonexistent", null); Mockito.verify(aMasterTrigger, Mockito.never()).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: nonexistent", this.gitStatus.toString()); + } + + @Test + public void testDoNotifyCommitWithSlashesInBranchNames() throws Exception { + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + + SCMTrigger aSlashesTrigger = setupProjectWithTrigger("a", "name/with/slashes", false); + + this.gitStatus.doNotifyCommit(requestWithParameter, "a", "name/with/slashes", null); + Mockito.verify(aSlashesTrigger).run(); + Mockito.verify(bMasterTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: name/with/slashes", this.gitStatus.toString()); } + @Test + public void testDoNotifyCommitWithParametrizedBranch() throws Exception { + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "$BRANCH_TO_BUILD", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); + + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); + Mockito.verify(aMasterTrigger).run(); + Mockito.verify(bMasterTrigger, Mockito.never()).run(); + Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + assertEquals("URL: a Branches: master", this.gitStatus.toString()); + } + + @Test public void testDoNotifyCommitWithIgnoredRepository() throws Exception { - SCMTrigger aMasterTrigger = setupProject("a", "master", true); + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", true); - this.gitStatus.doNotifyCommit("a", null, ""); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, ""); Mockito.verify(aMasterTrigger, Mockito.never()).run(); - } + assertEquals("URL: a SHA1: ", this.gitStatus.toString()); + } + @Test public void testDoNotifyCommitWithNoScmTrigger() throws Exception { setupProject("a", "master", null); - this.gitStatus.doNotifyCommit("a", null, ""); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, ""); // no expectation here, however we shouldn't have a build triggered, and no exception + + assertEquals("URL: a SHA1: ", this.gitStatus.toString()); } - private SCMTrigger setupProject(String url, String branchString, boolean ignoreNotifyCommit) throws Exception { + @Test + public void testDoNotifyCommitWithTwoBranchesAndAdditionalParameterAllowed() throws Exception { + doNotifyCommitWithTwoBranchesAndAdditionalParameter(true, null); + } + + @Test + public void testDoNotifyCommitWithTwoBranchesAndAdditionalParameter() throws Exception { + doNotifyCommitWithTwoBranchesAndAdditionalParameter(false, null); + } + + @Test + public void testDoNotifyCommitWithTwoBranchesAndAdditionalSafeParameter() throws Exception { + doNotifyCommitWithTwoBranchesAndAdditionalParameter(false, "paramKey1"); + } + + @Test + public void testDoNotifyCommitWithTwoBranchesAndAdditionalUnsafeParameter() throws Exception { + doNotifyCommitWithTwoBranchesAndAdditionalParameter(false, "does,not,include,param"); + } + + private void doNotifyCommitWithTwoBranchesAndAdditionalParameter(final boolean allowed, String safeParameters) throws Exception { + if (allowed) { + GitStatus.setAllowNotifyCommitParameters(true); + } + + boolean allowedParamKey1 = allowed; + if (safeParameters != null) { + GitStatus.setSafeParametersForTest(safeParameters); + if (safeParameters.contains("paramKey1")) { + allowedParamKey1 = true; + } + } + + SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", false); + SCMTrigger aTopicTrigger = setupProjectWithTrigger("a", "topic", false); + SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); + SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); + + Map parameterMap = new HashMap<>(); + parameterMap.put("paramKey1", new String[] {"paramValue1"}); + when(requestWithParameter.getParameterMap()).thenReturn(parameterMap); + + this.gitStatus.doNotifyCommit(requestWithParameter, "a", "master,topic", null); + Mockito.verify(aMasterTrigger).run(); + Mockito.verify(aTopicTrigger).run(); + Mockito.verify(bMasterTrigger, Mockito.never()).run(); + Mockito.verify(bTopicTrigger, Mockito.never()).run(); + + String expected = "URL: a Branches: master,topic" + + (allowedParamKey1 ? " Parameters: paramKey1='paramValue1'" : "") + + (allowedParamKey1 ? " More parameters: paramKey1='paramValue1'" : ""); + assertEquals(expected, this.gitStatus.toString()); + } + + @DataPoints("branchSpecPrefixes") + public static final String[] BRANCH_SPEC_PREFIXES = new String[] { + "", + "refs/remotes/", + "refs/heads/", + "origin/", + "remotes/origin/" + }; + + @Theory + public void testDoNotifyCommitBranchWithSlash(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { + SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "feature/awesome-feature", false); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + + Mockito.verify(trigger).run(); + } + + @Theory + public void testDoNotifyCommitBranchWithoutSlash(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { + SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "awesome-feature", false); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "awesome-feature", null); + + Mockito.verify(trigger).run(); + } + + @Theory + public void testDoNotifyCommitBranchByBranchRef(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { + SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "awesome-feature", false); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "refs/heads/awesome-feature", null); + + Mockito.verify(trigger).run(); + } + + @Test + public void testDoNotifyCommitBranchWithRegex() throws Exception { + SCMTrigger trigger = setupProjectWithTrigger("remote", ":[^/]*/awesome-feature", false); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + + Mockito.verify(trigger).run(); + } + + @Test + public void testDoNotifyCommitBranchWithWildcard() throws Exception { + SCMTrigger trigger = setupProjectWithTrigger("remote", "origin/feature/*", false); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + + Mockito.verify(trigger).run(); + } + + private void assertAdditionalParameters(Collection actions) { + for (Action action: actions) { + if (action instanceof ParametersAction) { + final List parameters = ((ParametersAction) action).getParameters(); + assertEquals(2, parameters.size()); + for (ParameterValue value : parameters) { + assertTrue((value.getName().equals("paramKey1") && value.getValue().equals("paramValue1")) + || (value.getName().equals("paramKey2") && value.getValue().equals("paramValue2"))); + } + } + } + } + + private SCMTrigger setupProjectWithTrigger(String url, String branchString, boolean ignoreNotifyCommit) throws Exception { SCMTrigger trigger = Mockito.mock(SCMTrigger.class); Mockito.doReturn(ignoreNotifyCommit).when(trigger).isIgnorePostCommitHooks(); setupProject(url, branchString, trigger); @@ -138,7 +360,7 @@ private SCMTrigger setupProject(String url, String branchString, boolean ignoreN } private void setupProject(String url, String branchString, SCMTrigger trigger) throws Exception { - FreeStyleProject project = createFreeStyleProject(); + FreeStyleProject project = jenkins.createFreeStyleProject(); GitSCM git = new GitSCM( Collections.singletonList(new UserRemoteConfig(url, null, null, null)), Collections.singletonList(new BranchSpec(branchString)), @@ -149,24 +371,255 @@ private void setupProject(String url, String branchString, SCMTrigger trigger) t if (trigger != null) project.addTrigger(trigger); } - - public void testLooseMatch() throws URISyntaxException { - String[] list = new String[]{ + @WithoutJenkins + @Test + public void testLooselyMatches() throws URISyntaxException { + String[] equivalentRepoURLs = new String[]{ + "https://github.com/jenkinsci/git-plugin", + "https://github.com/jenkinsci/git-plugin/", "https://github.com/jenkinsci/git-plugin.git", - "git://github.com/jenkinsci/git-plugin.git", - "ssh://git@github.com/jenkinsci/git-plugin.git", + "https://github.com/jenkinsci/git-plugin.git/", "https://someone@github.com/jenkinsci/git-plugin.git", - "git@github.com:jenkinsci/git-plugin.git" + "https://someone:somepassword@github.com/jenkinsci/git-plugin/", + "git://github.com/jenkinsci/git-plugin", + "git://github.com/jenkinsci/git-plugin/", + "git://github.com/jenkinsci/git-plugin.git", + "git://github.com/jenkinsci/git-plugin.git/", + "ssh://git@github.com/jenkinsci/git-plugin", + "ssh://github.com/jenkinsci/git-plugin.git", + "git@github.com:jenkinsci/git-plugin/", + "git@github.com:jenkinsci/git-plugin.git", + "git@github.com:jenkinsci/git-plugin.git/" }; - List uris = new ArrayList(); - for (String s : list) { - uris.add(new URIish(s)); + List uris = new ArrayList<>(); + for (String testURL : equivalentRepoURLs) { + uris.add(new URIish(testURL)); } + /* Extra slashes on end of URL probably should be considered equivalent, + * but current implementation does not consider them as loose matches + */ + URIish badURLTrailingSlashes = new URIish(equivalentRepoURLs[0] + "///"); + /* Different hostname should always fail match check */ + URIish badURLHostname = new URIish(equivalentRepoURLs[0].replace("github.com", "bitbucket.org")); + for (URIish lhs : uris) { + assertFalse(lhs + " matches trailing slashes " + badURLTrailingSlashes, GitStatus.looselyMatches(lhs, badURLTrailingSlashes)); + assertFalse(lhs + " matches bad hostname " + badURLHostname, GitStatus.looselyMatches(lhs, badURLHostname)); for (URIish rhs : uris) { - assertTrue(lhs+" and "+rhs+" didn't match",new GitStatus().looselyMatches(lhs,rhs)); + assertTrue(lhs + " and " + rhs + " didn't match", GitStatus.looselyMatches(lhs, rhs)); + } + } + } + + private FreeStyleProject setupNotifyProject() throws Exception { + FreeStyleProject project = jenkins.createFreeStyleProject(); + project.setQuietPeriod(0); + GitSCM git = new GitSCM( + Collections.singletonList(new UserRemoteConfig(repoURL, null, null, null)), + Collections.singletonList(new BranchSpec(branch)), + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + project.setScm(git); + project.addTrigger(new SCMTrigger("")); // Required for GitStatus to see polling request + return project; + } + + private Map setupParameterMap() { + Map parameterMap = new HashMap<>(); + String[] repoURLs = {repoURL}; + parameterMap.put("url", repoURLs); + String[] branches = {branch}; + parameterMap.put("branches", branches); + String[] hashes = {sha1}; + parameterMap.put("sha1", hashes); + return parameterMap; + } + + private Map setupParameterMap(String extraValue) { + Map parameterMap = setupParameterMap(); + String[] extra = {extraValue}; + parameterMap.put("extra", extra); + return parameterMap; + } + + @Test + public void testDoNotifyCommit() throws Exception { /* No parameters */ + setupNotifyProject(); + this.gitStatus.doNotifyCommit(requestWithNoParameter, repoURL, branch, sha1); + assertEquals("URL: " + repoURL + + " SHA1: " + sha1 + + " Branches: " + branch, this.gitStatus.toString()); + } + + @Test + public void testDoNotifyCommitWithExtraParameterAllowed() throws Exception { + doNotifyCommitWithExtraParameterAllowed(true, null); + } + + @Test + public void testDoNotifyCommitWithExtraParameter() throws Exception { + doNotifyCommitWithExtraParameterAllowed(false, null); + } + + @Test + public void testDoNotifyCommitWithExtraSafeParameter() throws Exception { + doNotifyCommitWithExtraParameterAllowed(false, "something,extra,is,here"); + } + + @Test + public void testDoNotifyCommitWithExtraUnsafeParameter() throws Exception { + doNotifyCommitWithExtraParameterAllowed(false, "something,is,not,here"); + } + + private void doNotifyCommitWithExtraParameterAllowed(final boolean allowed, String safeParameters) throws Exception { + if (allowed) { + GitStatus.setAllowNotifyCommitParameters(true); + } + + boolean allowedExtra = allowed; + if (safeParameters != null) { + GitStatus.setSafeParametersForTest(safeParameters); + if (safeParameters.contains("extra")) { + allowedExtra = true; + } + } + setupNotifyProject(); + String extraValue = "An-extra-value"; + when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(extraValue)); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + + String expected = "URL: " + repoURL + + " SHA1: " + sha1 + + " Branches: " + branch + + (allowedExtra ? " Parameters: extra='" + extraValue + "'" : "") + + (allowedExtra ? " More parameters: extra='" + extraValue + "'" : ""); + assertEquals(expected, this.gitStatus.toString()); + } + + @Test + public void testDoNotifyCommitWithNullValueExtraParameter() throws Exception { + setupNotifyProject(); + when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(null)); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + assertEquals("URL: " + repoURL + + " SHA1: " + sha1 + + " Branches: " + branch, this.gitStatus.toString()); + } + + @Test + public void testDoNotifyCommitWithDefaultParameterAllowed() throws Exception { + doNotifyCommitWithDefaultParameter(true, null); + } + + @Test + public void testDoNotifyCommitWithDefaultParameter() throws Exception { + doNotifyCommitWithDefaultParameter(false, null); + } + + @Test + public void testDoNotifyCommitWithDefaultSafeParameter() throws Exception { + doNotifyCommitWithDefaultParameter(false, "A,B,C,extra"); + } + + @Test + public void testDoNotifyCommitWithDefaultUnsafeParameterC() throws Exception { + doNotifyCommitWithDefaultParameter(false, "A,B,extra"); + } + + @Test + public void testDoNotifyCommitWithDefaultUnsafeParameterExtra() throws Exception { + doNotifyCommitWithDefaultParameter(false, "A,B,C"); + } + + private void doNotifyCommitWithDefaultParameter(final boolean allowed, String safeParameters) throws Exception { + assumeTrue(runUnreliableTests()); // Test cleanup is unreliable in some cases + if (allowed) { + GitStatus.setAllowNotifyCommitParameters(true); + } + + boolean allowedExtra = allowed; + if (safeParameters != null) { + GitStatus.setSafeParametersForTest(safeParameters); + if (safeParameters.contains("extra")) { + allowedExtra = true; } } + + // Use official repo for this single test + this.repoURL = "https://github.com/jenkinsci/git-plugin.git"; + FreeStyleProject project = setupNotifyProject(); + project.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("A", "aaa"), + new StringParameterDefinition("C", "ccc"), + new StringParameterDefinition("B", "$A$C") + )); + final CommandInterpreter script = isWindows() + ? new BatchFile("echo %A% %B% %C%") + : new Shell("echo $A $B $C"); + project.getBuildersList().add(script); + + FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserCause()).get(); + + jenkins.assertLogContains("aaa aaaccc ccc", build); + + String extraValue = "An-extra-value"; + when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(extraValue)); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + + String expected = "URL: " + repoURL + + " SHA1: " + sha1 + + " Branches: " + branch + + (allowedExtra ? " Parameters: extra='" + extraValue + "'" : "") + + " More parameters: " + + (allowedExtra ? "extra='" + extraValue + "'," : "") + + "A='aaa',C='ccc',B='$A$C'"; + assertEquals(expected, this.gitStatus.toString()); + } + + /** Returns true if unreliable tests should be run */ + private boolean runUnreliableTests() { + if (!isWindows()) { + return true; // Always run tests on non-Windows platforms + } + String jobUrl = System.getenv("JOB_URL"); + if (jobUrl == null) { + return true; // Always run tests when not inside a CI environment + } + return !jobUrl.contains("ci.jenkins.io"); // Skip some tests on ci.jenkins.io, windows cleanup is unreliable on those machines + } + + /** + * inline ${@link hudson.Functions#isWindows()} to prevent a transient + * remote classloader issue + */ + private boolean isWindows() { + return File.pathSeparatorChar == ';'; + } + + @Test + @Issue("JENKINS-46929") + public void testDoNotifyCommitTriggeredHeadersLimited() throws Exception { + SCMTrigger[] projectTriggers = new SCMTrigger[50]; + for (int i = 0; i < projectTriggers.length; i++) { + projectTriggers[i] = setupProjectWithTrigger("a", "master", false); + } + + HttpResponse rsp = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); + + // Up to 10 "Triggered" headers + 1 extra warning are returned. + StaplerRequest sReq = mock(StaplerRequest.class); + StaplerResponse sRsp = mock(StaplerResponse.class); + Mockito.when(sRsp.getWriter()).thenReturn(mock(PrintWriter.class)); + rsp.generateResponse(sReq, sRsp, null); + Mockito.verify(sRsp, Mockito.times(11)).addHeader(Mockito.eq("Triggered"), Mockito.anyString()); + + // All triggers run. + for (SCMTrigger projectTrigger : projectTriggers) { + Mockito.verify(projectTrigger).run(); + } + + assertEquals("URL: a Branches: master", this.gitStatus.toString()); } } diff --git a/src/test/java/hudson/plugins/git/GitTagActionTest.java b/src/test/java/hudson/plugins/git/GitTagActionTest.java new file mode 100644 index 0000000000..57180ae7ab --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitTagActionTest.java @@ -0,0 +1,392 @@ +package hudson.plugins.git; + +import java.io.File; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.Descriptor; +import hudson.model.FreeStyleProject; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.Branch; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.LocalBranch; + +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import jenkins.plugins.git.GitSampleRepoRule; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Test git tag action. Low value test that was created as part of + * another investigation. + * + * Unreliable on ci.jenkins.io Windows agents. Results are not worth + * sacrificing other things in order to investigate. Runs reliably on + * Unix-like operating systems. Runs reliably on Mark Waite's windows + * computers. + * + * @author Mark Waite + */ +public class GitTagActionTest { + + private static GitTagAction noTagAction; + private static GitTagAction tagOneAction; + private static GitTagAction tagTwoAction; + + private static final Random random = new Random(); + + @ClassRule + public static JenkinsRule r = new JenkinsRule(); + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @ClassRule + public static GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + public GitTagActionTest() { + } + + private static FreeStyleProject p; + private static GitClient workspaceGitClient = null; + + private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("-yyyy-MM-dd-H-m-ss.SS"); + private static final String TAG_PREFIX = "test-tag-"; + private static final String TAG_SUFFIX = LocalDateTime.now().format(FORMAT); + private static final String INITIAL_COMMIT_MESSAGE = "init" + TAG_SUFFIX + "-" + random.nextInt(10000); + private static final String ADDED_COMMIT_MESSAGE_BASE = "added" + TAG_SUFFIX; + private static String sampleRepoHead = null; + + @BeforeClass + public static void deleteMatchingTags() throws Exception { + if (isWindows()) { // Test is unreliable on Windows, too low value to investigate further + return; + } + /* Remove tags from working repository that start with TAG_PREFIX and don't contain TAG_SUFFIX */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(new File(".")) + .using(chooseGitImplementation()) // Use random implementation, both should work + .getClient(); + for (GitObject tag : gitClient.getTags()) { + if (tag.getName().startsWith(TAG_PREFIX) && !tag.getName().contains(TAG_SUFFIX)) { + gitClient.deleteTag(tag.getName()); + } + } + } + + @BeforeClass + public static void createThreeGitTagActions() throws Exception { + if (isWindows()) { // Test is unreliable on Windows, too low value to investigate further + return; + } + sampleRepo.init(); + sampleRepo.write("file", INITIAL_COMMIT_MESSAGE); + sampleRepo.git("commit", "--all", "--message=" + INITIAL_COMMIT_MESSAGE); + sampleRepoHead = sampleRepo.head(); + List remotes = new ArrayList<>(); + String refSpec = "+refs/heads/master:refs/remotes/origin/master"; + remotes.add(new UserRemoteConfig(sampleRepo.fileUrl(), "origin", refSpec, "")); + GitSCM scm = new GitSCM( + remotes, + Collections.singletonList(new BranchSpec("origin/master")), + false, Collections.emptyList(), + null, + chooseGitImplementation(), // Both git implementations should work, choose randomly + Collections.emptyList()); + scm.getExtensions().add(new LocalBranch("master")); + p = r.createFreeStyleProject(); + p.setScm(scm); + + /* Run with no tag action defined */ + noTagAction = createTagAction(null); + + /* Run with first tag action defined */ + tagOneAction = createTagAction("v1"); + + /* Wait for tag creation threads to complete, then assert conditions */ + waitForTagCreation(tagOneAction, "v1"); + + /* Run with second tag action defined */ + tagTwoAction = createTagAction("v2"); + + /* Wait for tag creation threads to complete, then assert conditions */ + waitForTagCreation(tagTwoAction, "v2"); + + assertThat(getMatchingTagNames(), hasItems(getTagValue("v1"), getTagValue("v2"))); + } + + private static String getTagName(String message) { + return TAG_PREFIX + message + TAG_SUFFIX; + } + + private static String getTagValue(String message) { + return getTagName(message) + "-value"; + } + + private static String getTagComment(String message) { + return getTagName(message) + "-comment"; + } + + private static int messageCounter = 1; + + /** + * Return a GitTagAction which uses 'message' in the tag name, tag value, and tag comment. + * If 'message' is null, the GitTagAction is returned but tag creation is not scheduled. + * + * @param message value to use in tag name, value, and comment when scheduling tag creation. If null, tag is not created. + * @return tag action which uses 'message' in the tag name, value, and comment + * @throws Exception on error + */ + private static GitTagAction createTagAction(String message) throws Exception { + /* Run with a tag action defined */ + String commitMessage = message == null ? ADDED_COMMIT_MESSAGE_BASE + "-" + messageCounter++ : message; + sampleRepo.write("file", message); + sampleRepo.git("commit", "--all", "--message=" + commitMessage); + List masterBranchList = new ArrayList<>(); + ObjectId tagObjectId = ObjectId.fromString(sampleRepo.head()); + masterBranchList.add(new Branch("master", tagObjectId)); + Revision tagRevision = new Revision(tagObjectId, masterBranchList); + + /* Run the freestyle project and compute its workspace FilePath */ + Run tagRun = r.buildAndAssertSuccess(p); + FilePath workspace = r.jenkins.getWorkspaceFor(p); + + /* Create a GitClient for the workspace */ + if (workspaceGitClient == null) { + /* Assumes workspace does not move after first run */ + workspaceGitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(workspace) + .using(chooseGitImplementation()) // Use random implementation, both should work + .getClient(); + } + /* Fail if the workspace moved */ + assertThat(workspace, is(workspaceGitClient.getWorkTree())); + + /* Fail if initial commit and subsequent commit not detected in workspace */ + StringWriter stringWriter = new StringWriter(); + workspaceGitClient.changelog(sampleRepoHead + "^", "HEAD", stringWriter); + assertThat(stringWriter.toString(), containsString(INITIAL_COMMIT_MESSAGE)); + assertThat(stringWriter.toString(), containsString(commitMessage)); + + /* Fail if master branch is not defined in the workspace */ + assertThat(workspaceGitClient.getRemoteUrl("origin"), is(sampleRepo.fileUrl().replace("file:/", "file:///"))); + Set branches = workspaceGitClient.getBranches(); + if (branches.isEmpty()) { + /* Should not be required since the LocalBranch extension was enabled */ + workspaceGitClient.branch("master"); + branches = workspaceGitClient.getBranches(); + assertThat(branches, is(not(empty()))); + } + boolean foundMasterBranch = false; + String lastBranchName = null; + for (Branch branch : branches) { + lastBranchName = branch.getName(); + assertThat(lastBranchName, endsWith("master")); + if (lastBranchName.equals("master")) { + foundMasterBranch = true; + } + } + assertTrue("master branch not found, last branch name was " + lastBranchName, foundMasterBranch); + + /* Create the GitTagAction */ + GitTagAction tagAction = new GitTagAction(tagRun, workspace, tagRevision); + + /* Schedule tag creation if message is not null */ + if (message != null) { + String tagName = getTagName(message); + String tagValue = getTagValue(message); + String tagComment = getTagComment(message); + Map tagMap = new HashMap<>(); + tagMap.put(tagName, tagValue); + tagAction.scheduleTagCreation(tagMap, tagComment); + } + return tagAction; + } + + private static Set getMatchingTagNames() throws Exception { + Set tags = workspaceGitClient.getTags(); + Set matchingTagNames = new HashSet<>(); + for (GitObject tag : tags) { + if (tag.getName().startsWith(TAG_PREFIX)) { + matchingTagNames.add(tag.getName()); + } + } + return matchingTagNames; + } + + private static void waitForTagCreation(GitTagAction tagAction, String message) throws Exception { + long backoffDelay = 499L; + while (tagAction.getLastTagName() == null && tagAction.getLastTagException() == null && backoffDelay < 8000L) { + backoffDelay = backoffDelay * 2; + Thread.sleep(backoffDelay); // Allow some time for tag creation + } + assertThat(tagAction.getLastTagException(), is(nullValue())); + assertThat(tagAction.getLastTagName(), is(getTagValue(message))); + } + + @Test + public void testDoPost() throws Exception { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + JenkinsRule.WebClient browser = r.createWebClient(); + + // Don't need all cases until at least one case works fully + // HtmlPage tagPage = browser.getPage(p, "/1/tagBuild"); + // HtmlForm form = tagPage.getFormByName("tag"); + // form.getInputByName("name0").setValueAttribute("tag-build-1"); + // HtmlPage submitted = r.submit(form); + + // Flaw in the test causes this assertion to fail + // assertThat(submitted.asText(), not(containsString("Clear error to retry"))); + + // Don't need all cases until at least one case works fully + // HtmlPage tagPage2 = browser.getPage(p, "/2/tagBuild"); + // HtmlForm form2 = tagPage2.getFormByName("tag"); + // form2.getInputByName("name0").setValueAttribute("tag-build-2"); + // HtmlPage submitted2 = r.submit(form2); + + // Flaw in the test causes this assertion to fail + // assertThat(submitted2.asText(), not(containsString("Clear error to retry"))); + + HtmlPage tagPage3 = browser.getPage(p, "/3/tagBuild"); + HtmlForm form3 = tagPage3.getFormByName("tag"); + form3.getInputByName("name0").setValueAttribute("tag-build-3"); + HtmlPage submitted3 = r.submit(form3); + + // Flaw in the test causes this assertion to fail + // assertThat(submitted3.asText(), not(containsString("Clear error to retry"))); + + // Flaw in the test causes this assertion to fail + // waitForTagCreation(tagTwoAction); + // assertThat(getMatchingTagNames(), hasItems("tag-build-1", "tag-build-2", "tag-build-3")); + } + + @Test + public void testGetDescriptor() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + Descriptor descriptor = noTagAction.getDescriptor(); + assertThat(descriptor.getDisplayName(), is("Tag")); + } + + // @Test + public void testIsTagged() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertTrue(tagTwoAction.isTagged()); + } + + @Test + public void testIsNotTagged() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertFalse(noTagAction.isTagged()); + } + + @Test + public void testGetDisplayNameNoTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(noTagAction.getDisplayName(), is("No Tags")); + } + + // Not working yet + // @Test + public void testGetDisplayNameOneTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(tagOneAction.getDisplayName(), is("One Tag")); + } + + // Not working yet + // @Test + public void testGetDisplayNameTwoTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(tagTwoAction.getDisplayName(), is("Multiple Tags")); + } + + @Test + public void testGetIconFileName() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(noTagAction.getIconFileName(), is("save.gif")); + } + + @Test + public void testGetTagsNoTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + Collection> valueList = noTagAction.getTags().values(); + for (List value : valueList) { + assertThat(value, is(empty())); + } + } + + @Test + public void testGetTagsOneTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + Collection> valueList = tagOneAction.getTags().values(); + for (List value : valueList) { + assertThat(value, is(empty())); + } + } + + @Test + public void testGetTagsTwoTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + Collection> valueList = tagTwoAction.getTags().values(); + for (List value : valueList) { + assertThat(value, is(empty())); + } + } + + @Test + public void testGetTagInfo() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(noTagAction.getTagInfo(), is(empty())); + } + + @Test + public void testGetTooltipNoTagAction() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(noTagAction.getTooltip(), is(nullValue())); + } + + @Test + public void testGetPermission() { + assumeTrue(!isWindows()); // Test is unreliable on Windows, too low value to investigate further + assertThat(noTagAction.getPermission(), is(GitSCM.TAG)); + assertThat(tagOneAction.getPermission(), is(GitSCM.TAG)); + } + + private static String chooseGitImplementation() { + return random.nextBoolean() ? "git" : "jgit"; + } + + /** + * inline ${@link hudson.Functions#isWindows()} to prevent a transient + * remote classloader issue + */ + private static boolean isWindows() { + return File.pathSeparatorChar == ';'; + } +} diff --git a/src/test/java/hudson/plugins/git/JGitSCMTriggerLocalPollTest.java b/src/test/java/hudson/plugins/git/JGitSCMTriggerLocalPollTest.java new file mode 100644 index 0000000000..aeb63a5b30 --- /dev/null +++ b/src/test/java/hudson/plugins/git/JGitSCMTriggerLocalPollTest.java @@ -0,0 +1,21 @@ +package hudson.plugins.git; + +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.impl.EnforceGitClient; + +public class JGitSCMTriggerLocalPollTest extends SCMTriggerTest +{ + + @Override + protected EnforceGitClient getGitClient() + { + return new EnforceGitClient().set(GitClientType.JGIT); + } + + @Override + protected boolean isDisableRemotePoll() + { + return true; + } + +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/git/JGitSCMTriggerRemotePollTest.java b/src/test/java/hudson/plugins/git/JGitSCMTriggerRemotePollTest.java new file mode 100644 index 0000000000..b8f8d6108a --- /dev/null +++ b/src/test/java/hudson/plugins/git/JGitSCMTriggerRemotePollTest.java @@ -0,0 +1,26 @@ +package hudson.plugins.git; + +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.impl.EnforceGitClient; + +/** + * Remote polling and local polling behave differently due to bugs in productive + * code which probably cannot be fixed without serious compatibility problems. + * The isChangeExpected() method adjusts the tests to the difference between + * local and remote polling. + */ +public class JGitSCMTriggerRemotePollTest extends SCMTriggerTest +{ + @Override + protected EnforceGitClient getGitClient() + { + return new EnforceGitClient().set(GitClientType.JGIT); + } + + @Override + protected boolean isDisableRemotePoll() + { + return false; + } + +} diff --git a/src/test/java/hudson/plugins/git/MultipleSCMTest.java b/src/test/java/hudson/plugins/git/MultipleSCMTest.java index 17375979c0..a6c45942cb 100644 --- a/src/test/java/hudson/plugins/git/MultipleSCMTest.java +++ b/src/test/java/hudson/plugins/git/MultipleSCMTest.java @@ -15,63 +15,70 @@ import hudson.scm.SCM; import org.jenkinsci.plugins.multiplescms.MultiSCM; -import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; /** * Verifies the git plugin interacts correctly with the multiple SCMs plugin. * * @author corey@ooyala.com */ -public class MultipleSCMTest extends HudsonTestCase { +public class MultipleSCMTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + protected TaskListener listener; protected TestGitRepo repo0; protected TestGitRepo repo1; - protected void setUp() throws Exception { - super.setUp(); - + @Before public void setUp() throws Exception { listener = StreamTaskListener.fromStderr(); - repo0 = new TestGitRepo("repo0", this, listener); - repo1 = new TestGitRepo("repo1", this, listener); + repo0 = new TestGitRepo("repo0", tmp.newFolder(), listener); + repo1 = new TestGitRepo("repo1", tmp.newFolder(), listener); } - - public void testBasic() throws Exception + + @Test public void basic() throws Exception { FreeStyleProject project = setupBasicProject("master"); repo0.commit("repo0-init", repo0.johnDoe, "repo0 initial commit"); assertTrue("scm polling should detect a change after initial commit", - project.pollSCMChanges(listener)); + project.poll(listener).hasChanges()); repo1.commit("repo1-init", repo1.janeDoe, "repo1 initial commit"); build(project, Result.SUCCESS); assertFalse("scm polling should not detect any more changes after build", - project.pollSCMChanges(listener)); + project.poll(listener).hasChanges()); repo1.commit("repo1-1", repo1.johnDoe, "repo1 commit 1"); build(project, Result.SUCCESS); assertFalse("scm polling should not detect any more changes after build", - project.pollSCMChanges(listener)); + project.poll(listener).hasChanges()); repo0.commit("repo0-1", repo0.janeDoe, "repo0 commit 1"); build(project, Result.SUCCESS); assertFalse("scm polling should not detect any more changes after build", - project.pollSCMChanges(listener)); + project.poll(listener).hasChanges()); } private FreeStyleProject setupBasicProject(String name) throws IOException { - FreeStyleProject project = createFreeStyleProject(name); + FreeStyleProject project = r.createFreeStyleProject(name); List branch = Collections.singletonList(new BranchSpec("master")); @@ -93,7 +100,7 @@ private FreeStyleProject setupBasicProject(String name) throws IOException null, Collections.emptyList()); - List testScms = new ArrayList(); + List testScms = new ArrayList<>(); testScms.add(repo0Scm); testScms.add(repo1Scm); @@ -106,9 +113,9 @@ private FreeStyleProject setupBasicProject(String name) throws IOException private FreeStyleBuild build(final FreeStyleProject project, final Result expectedResult) throws Exception { - final FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserCause()).get(); + final FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserIdCause()).get(); if(expectedResult != null) { - assertBuildStatus(expectedResult, build); + r.assertBuildStatus(expectedResult, build); } return build; } diff --git a/src/test/java/hudson/plugins/git/RevisionParameterActionRemoteUrlTest.java b/src/test/java/hudson/plugins/git/RevisionParameterActionRemoteUrlTest.java new file mode 100644 index 0000000000..0f23dc472c --- /dev/null +++ b/src/test/java/hudson/plugins/git/RevisionParameterActionRemoteUrlTest.java @@ -0,0 +1,66 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RevisionParameterActionRemoteUrlTest { + + @Test + public void noRemoteURLSet() throws Exception { + RevisionParameterAction target = new RevisionParameterAction("sha1"); + URIish remoteURL = new URIish("https://github.com/jenkinsci/git-plugin.git"); + assertTrue("should always return true when no remote set", target.canOriginateFrom(remotes(remoteURL))); + } + + @Test + public void remoteURLSetButDoesntMatch() throws Exception { + URIish actionURL = new URIish("https://github.com/jenkinsci/multiple-scms-plugin.git"); + RevisionParameterAction target = new RevisionParameterAction("sha1", actionURL); + + URIish remoteURL = new URIish("https://github.com/jenkinsci/git-plugin.git"); + assertFalse("should return false on different remotes", target.canOriginateFrom(remotes(remoteURL))); + } + + @Test + public void remoteURLSetAndMatches() throws Exception { + URIish actionURL = new URIish("https://github.com/jenkinsci/git-plugin.git"); + RevisionParameterAction target = new RevisionParameterAction("sha1", actionURL); + + URIish remoteURL = new URIish("https://github.com/jenkinsci/git-plugin.git"); + assertTrue("should return true on same remotes", target.canOriginateFrom(remotes(remoteURL))); + } + + @Test + public void multipleRemoteURLsSetAndOneMatches() throws Exception { + URIish actionURL = new URIish("https://github.com/jenkinsci/git-plugin.git"); + RevisionParameterAction target = new RevisionParameterAction("sha1", actionURL); + + URIish remoteURL1 = new URIish("https://github.com/jenkinsci/multiple-scms-plugin.git"); + URIish remoteURL2 = new URIish("https://github.com/jenkinsci/git-plugin.git"); + assertTrue("should return true when any remote matches", target.canOriginateFrom(remotes(remoteURL1, remoteURL2))); + } + + private List remotes(URIish... remoteURLs) { + List result = new ArrayList<>(); + for (URIish remoteURL : remoteURLs) { + result.add(remote(remoteURL)); + } + return result; + } + + private RemoteConfig remote(URIish remoteURL) { + RemoteConfig result = mock(RemoteConfig.class); + when(result.getURIs()).thenReturn(Arrays.asList(remoteURL)); + return result; + } +} diff --git a/src/test/java/hudson/plugins/git/RevisionParameterActionTest.java b/src/test/java/hudson/plugins/git/RevisionParameterActionTest.java index e1594a05d8..e9465ed5df 100644 --- a/src/test/java/hudson/plugins/git/RevisionParameterActionTest.java +++ b/src/test/java/hudson/plugins/git/RevisionParameterActionTest.java @@ -29,186 +29,50 @@ import hudson.model.Result; import hudson.plugins.git.util.BuildData; -import org.eclipse.jgit.lib.ObjectId; -import org.jvnet.hudson.test.HudsonTestCase; - -import java.util.concurrent.Future; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.List; +import static org.junit.Assert.*; +import org.junit.Test; /** * Tests for {@link RevisionParameterAction} - * + * * @author Chris Johnson */ -public class RevisionParameterActionTest extends AbstractGitTestCase { - - /** - * Test covering the behaviour after 1.1.26 where passing different revision - * actions to a job in the queue creates separate builds - */ - public void testCombiningScheduling() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); - - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF"))); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("FREED456"))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNotNull(b2); - - // Check that two builds occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),2); - } - /** test when existing revision is already in the queue - */ - public void testCombiningScheduling2() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); - - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF"))); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF"))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNull(b2); // TODO fails in 1.521+ (along with other assertNull calls), perhaps due to fix of JENKINS-18407 - - // Check that only one build occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),1); - } - /** test when there is no revision on the item in the queue - */ - public void testCombiningScheduling3() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); +public class RevisionParameterActionTest extends AbstractGitProject { - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF"))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNotNull(b2); - - // Check that two builds occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),2); - } - - /** test when a different revision is already in the queue, and combine requests is required. - */ - public void testCombiningScheduling4() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); - - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF", true))); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("FFEEFFEE", true))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNull(b2); - - // Check that only one build occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),1); - - //check that the correct commit id is present in build - assertEquals(fs.getBuilds().get(0).getAction(RevisionParameterAction.class).commit, "FFEEFFEE"); - - } + @Test + public void testProvidingRevision() throws Exception { - /** test when a same revision is already in the queue, and combine requests is required. - */ - public void testCombiningScheduling5() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); - - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF", true))); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF", true))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNull(b2); - - // Check that only one build occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),1); - - //check that the correct commit id is present in build - assertEquals(fs.getBuilds().get(0).getAction(RevisionParameterAction.class).commit, "DEADBEEF"); - } - - /** test when a job already in the queue with no revision(manually started), and combine requests is required. - */ - public void testCombiningScheduling6() throws Exception { - - FreeStyleProject fs = createFreeStyleProject("freestyle"); - - // scheduleBuild2 returns null if request is combined into an existing item. (no new item added to queue) - Future b1 = fs.scheduleBuild2(20); - Future b2 = fs.scheduleBuild2(20, null, Collections.singletonList(new RevisionParameterAction("DEADBEEF", true))); - - // Check that we have the correct futures. - assertNotNull(b1); - assertNotNull(b2); - - // Check that two builds occurred - waitUntilNoActivity(); - assertEquals(fs.getBuilds().size(),2); - - //check that the correct commit id is present in 2nd build - // list is reversed indexed so first item is latest build - assertEquals(fs.getBuilds().get(0).getAction(RevisionParameterAction.class).commit, "DEADBEEF"); - } - - - public void testProvidingRevision() throws Exception { - - FreeStyleProject p1 = setupSimpleProject("master"); + FreeStyleProject p1 = setupSimpleProject("master"); // create initial commit and then run the build against it: final String commitFile1 = "commitFile1"; - commit(commitFile1, johnDoe, "Commit number 1"); + commitNewFile(commitFile1); FreeStyleBuild b1 = build(p1, Result.SUCCESS, commitFile1); - + Revision r1 = b1.getAction(BuildData.class).getLastBuiltRevision(); - + // create a second commit final String commitFile2 = "commitFile2"; - commit(commitFile2, janeDoe, "Commit number 2"); + commitNewFile(commitFile2); - // create second build and set revision parameter using r1 - FreeStyleBuild b2 = p1.scheduleBuild2(0, new Cause.UserCause(), + // create second build and set revision parameter using r1 + FreeStyleBuild b2 = p1.scheduleBuild2(0, new Cause.UserIdCause(), Collections.singletonList(new RevisionParameterAction(r1))).get(); - System.out.println(b2.getLog()); - - // Check revision built for b2 matches the r1 revision - assertEquals(b2.getAction(BuildData.class) - .getLastBuiltRevision().getSha1String(), r1.getSha1String()); - assertEquals(b2.getAction(BuildData.class) - .getLastBuiltRevision().getBranches().iterator().next() - .getName(), r1.getBranches().iterator().next().getName()); - - // create a third build - FreeStyleBuild b3 = build(p1, Result.SUCCESS, commitFile2); - - // Check revision built for b3 does not match r1 revision - assertFalse(b3.getAction(BuildData.class) - .getLastBuiltRevision().getSha1String().equals(r1.getSha1String())); - - if (System.getProperty("os.name").startsWith("Windows")) { - System.gc(); // Prevents exceptions cleaning up temp dirs during tearDown - } - } + // Check revision built for b2 matches the r1 revision + assertEquals(b2.getAction(BuildData.class) + .getLastBuiltRevision().getSha1String(), r1.getSha1String()); + assertEquals(b2.getAction(BuildData.class) + .getLastBuiltRevision().getBranches().iterator().next() + .getName(), r1.getBranches().iterator().next().getName()); + + // create a third build + FreeStyleBuild b3 = build(p1, Result.SUCCESS, commitFile2); + + // Check revision built for b3 does not match r1 revision + assertFalse(b3.getAction(BuildData.class) + .getLastBuiltRevision().getSha1String().equals(r1.getSha1String())); + } } diff --git a/src/test/java/hudson/plugins/git/SCMTriggerTest.java b/src/test/java/hudson/plugins/git/SCMTriggerTest.java new file mode 100644 index 0000000000..ea667f20c3 --- /dev/null +++ b/src/test/java/hudson/plugins/git/SCMTriggerTest.java @@ -0,0 +1,341 @@ +package hudson.plugins.git; + +import static java.util.Arrays.asList; +import static java.util.concurrent.TimeUnit.SECONDS; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.plugins.git.extensions.impl.EnforceGitClient; +import hudson.scm.PollingResult; +import hudson.triggers.SCMTrigger; +import hudson.util.RunList; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.Issue; + +public abstract class SCMTriggerTest extends AbstractGitProject +{ + private ZipFile namespaceRepoZip; + private Properties namespaceRepoCommits; + private ExecutorService singleThreadExecutor; + protected boolean expectChanges = false; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + expectChanges = false; + namespaceRepoZip = new ZipFile("src/test/resources/namespaceBranchRepo.zip"); + namespaceRepoCommits = parseLsRemote(new File("src/test/resources/namespaceBranchRepo.ls-remote")); + singleThreadExecutor = Executors.newSingleThreadExecutor(); + } + + protected abstract EnforceGitClient getGitClient(); + + protected abstract boolean isDisableRemotePoll(); + + @Test + public void testNamespaces_with_refsHeadsMaster() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/heads/master", + namespaceRepoCommits.getProperty("refs/heads/master"), + "origin/master"); + } + + // @Test + public void testNamespaces_with_remotesOriginMaster() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "remotes/origin/master", + namespaceRepoCommits.getProperty("refs/heads/master"), + "origin/master"); + } + + // @Test + public void testNamespaces_with_refsRemotesOriginMaster() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/remotes/origin/master", + namespaceRepoCommits.getProperty("refs/heads/master"), + "origin/master"); + } + + // @Test + public void testNamespaces_with_master() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "master", + namespaceRepoCommits.getProperty("refs/heads/master"), + "origin/master"); + } + + // @Test + public void testNamespaces_with_namespace1Master() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "a_tests/b_namespace1/master", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace1/master"), + "origin/a_tests/b_namespace1/master"); + } + + // @Test + public void testNamespaces_with_refsHeadsNamespace1Master() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/heads/a_tests/b_namespace1/master", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace1/master"), + "origin/a_tests/b_namespace1/master"); + } + + // @Test + public void testNamespaces_with_namespace2Master() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "a_tests/b_namespace2/master", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace2/master"), + "origin/a_tests/b_namespace2/master"); + } + + // @Test + public void testNamespaces_with_refsHeadsNamespace2Master() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/heads/a_tests/b_namespace2/master", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace2/master"), + "origin/a_tests/b_namespace2/master"); + } + + // @Test + public void testNamespaces_with_namespace3_feature3_sha1() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + "detached"); + } + + // @Test + public void testNamespaces_with_namespace3_feature3_branchName() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "a_tests/b_namespace3/feature3", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + "origin/a_tests/b_namespace3/feature3"); + } + + // @Test + public void testNamespaces_with_refsHeadsNamespace3_feature3_sha1() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + "detached"); + } + + // @Test + public void testNamespaces_with_refsHeadsNamespace3_feature3_branchName() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/heads/a_tests/b_namespace3/feature3", + namespaceRepoCommits.getProperty("refs/heads/a_tests/b_namespace3/feature3"), + "origin/a_tests/b_namespace3/feature3"); + } + + // @Test + public void testTags_with_TagA() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "TagA", + namespaceRepoCommits.getProperty("refs/tags/TagA"), + "TagA"); //TODO: What do we expect!? + } + + // @Test + public void testTags_with_TagBAnnotated() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "TagBAnnotated", + namespaceRepoCommits.getProperty("refs/tags/TagBAnnotated^{}"), + "TagBAnnotated"); //TODO: What do we expect!? + } + + // @Test + public void testTags_with_refsTagsTagA() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/tags/TagA", + namespaceRepoCommits.getProperty("refs/tags/TagA"), + "refs/tags/TagA"); //TODO: What do we expect!? + } + + // @Test + public void testTags_with_refsTagsTagBAnnotated() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/tags/TagBAnnotated", + namespaceRepoCommits.getProperty("refs/tags/TagBAnnotated^{}"), + "refs/tags/TagBAnnotated"); + } + + // @Test + public void testCommitAsBranchSpec_feature4_sha1() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + namespaceRepoCommits.getProperty("refs/heads/b_namespace3/feature4"), + namespaceRepoCommits.getProperty("refs/heads/b_namespace3/feature4"), + "detached"); + } + + // @Test + public void testCommitAsBranchSpec_feature4_branchName() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + "refs/heads/b_namespace3/feature4", + namespaceRepoCommits.getProperty("refs/heads/b_namespace3/feature4"), + "origin/b_namespace3/feature4"); + } + + // @Test + public void testCommitAsBranchSpec() throws Exception { + check(namespaceRepoZip, namespaceRepoCommits, + namespaceRepoCommits.getProperty("refs/heads/b_namespace3/master"), + namespaceRepoCommits.getProperty("refs/heads/b_namespace3/master"), + "detached"); + } + + @Issue("JENKINS-29796") + // @Test + public void testMultipleRefspecs() throws Exception { + final String remote = prepareRepo(namespaceRepoZip); + final UserRemoteConfig remoteConfig = new UserRemoteConfig(remote, "origin", + "+refs/pull/*:refs/remotes/origin/pr/* +refs/heads/*:refs/remotes/origin/*", null); + // First, build the master branch + String branchSpec = "refs/heads/master"; + FreeStyleProject project = setupProject(asList(remoteConfig), + asList(new BranchSpec(branchSpec)), + //empty scmTriggerSpec, SCMTrigger triggered manually + "", isDisableRemotePoll(), getGitClient()); + triggerSCMTrigger(project.getTrigger(SCMTrigger.class)); + FreeStyleBuild build1 = waitForBuildFinished(project, 1, 60000); + assertNotNull("Job has not been triggered", build1); + + // Now switch request a different branch + GitSCM scm = (GitSCM) project.getScm(); + scm.getBranches().set(0,new BranchSpec("b_namespace3/master")); + TaskListener listener = StreamTaskListener.fromStderr(); + + // Since the new branch has an additional commit, polling should report changes. Without the fix for + // JENKINS-29796, this assertion fails. + PollingResult poll = project.poll(listener); + assertEquals("Expected and actual polling results disagree", true, poll.hasChanges()); + } + + public void check(ZipFile repoZip, Properties commits, String branchSpec, + String expected_GIT_COMMIT, String expected_GIT_BRANCH) throws Exception { + String remote = prepareRepo(repoZip); + + FreeStyleProject project = setupProject(asList(new UserRemoteConfig(remote, null, null, null)), + asList(new BranchSpec(branchSpec)), + //empty scmTriggerSpec, SCMTrigger triggered manually + "", isDisableRemotePoll(), getGitClient()); + + //Speedup test - avoid waiting 1 minute + triggerSCMTrigger(project.getTrigger(SCMTrigger.class)); + + FreeStyleBuild build1 = waitForBuildFinished(project, 1, 60000); + assertNotNull("Job has not been triggered", build1); + + TaskListener listener = StreamTaskListener.fromStderr(); + PollingResult poll = project.poll(listener); + assertEquals("Expected and actual polling results disagree", false, poll.hasChanges()); + + //Speedup test - avoid waiting 1 minute + triggerSCMTrigger(project.getTrigger(SCMTrigger.class)).get(20, SECONDS); + + FreeStyleBuild build2 = waitForBuildFinished(project, 2, 2000); + assertNull("Found build 2 although no new changes and no multi candidate build", build2); + + assertEquals("Unexpected GIT_COMMIT", + expected_GIT_COMMIT, build1.getEnvironment(null).get("GIT_COMMIT")); + assertEquals("Unexpected GIT_BRANCH", + expected_GIT_BRANCH, build1.getEnvironment(null).get("GIT_BRANCH")); + } + + private String prepareRepo(ZipFile repoZip) throws IOException { + File tempRemoteDir = tempFolder.newFolder(); + extract(repoZip, tempRemoteDir); + return tempRemoteDir.getAbsolutePath(); + } + + private Future triggerSCMTrigger(final SCMTrigger trigger) + { + if(trigger == null) return null; + Callable callable = () -> { + trigger.run(); + return null; + }; + return singleThreadExecutor.submit(callable); + } + + private FreeStyleBuild waitForBuildFinished(FreeStyleProject project, int expectedBuildNumber, long timeout) + throws Exception + { + long endTime = System.currentTimeMillis() + timeout; + while(System.currentTimeMillis() < endTime) { + RunList builds = project.getBuilds(); + for(FreeStyleBuild build : builds) { + if(build.getNumber() == expectedBuildNumber) { + if(build.getResult() != null) return build; + break; //Wait until build finished + } + } + Thread.sleep(10); + } + return null; + } + + private Properties parseLsRemote(File file) throws IOException + { + Properties properties = new Properties(); + Pattern pattern = Pattern.compile("([a-f0-9]{40})\\s*(.*)"); + for(Object lineO : FileUtils.readLines(file)) { + String line = ((String)lineO).trim(); + Matcher matcher = pattern.matcher(line); + if(matcher.matches()) { + properties.setProperty(matcher.group(2), matcher.group(1)); + } else { + System.err.println("ls-remote pattern does not match '" + line + "'"); + } + } + return properties; + } + + private void extract(ZipFile zipFile, File outputDir) throws IOException + { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryDestination = new File(outputDir, entry.getName()); + entryDestination.getParentFile().mkdirs(); + if (entry.isDirectory()) + entryDestination.mkdirs(); + else { + try (InputStream in = zipFile.getInputStream(entry); + OutputStream out = Files.newOutputStream(entryDestination.toPath())) { + org.apache.commons.io.IOUtils.copy(in, out); + } + } + } + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return File.pathSeparatorChar==';'; + } +} diff --git a/src/test/java/hudson/plugins/git/SubmoduleCombinatorTest.java b/src/test/java/hudson/plugins/git/SubmoduleCombinatorTest.java new file mode 100644 index 0000000000..187f218cc3 --- /dev/null +++ b/src/test/java/hudson/plugins/git/SubmoduleCombinatorTest.java @@ -0,0 +1,76 @@ +package hudson.plugins.git; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +public class SubmoduleCombinatorTest { + + private SubmoduleCombinator combinator = null; + + public SubmoduleCombinatorTest() { + } + + @Before + public void setUp() throws IOException, InterruptedException { + TaskListener listener = StreamTaskListener.fromStderr(); + GitClient gitClient = Git.with(listener, new EnvVars()).in(new File(".")).getClient(); + Collection cfg = null; + combinator = new SubmoduleCombinator(gitClient, listener, cfg); + } + + @Test + public void testDifferenceNulls() { + Map item = new HashMap<>(); + List entries = new ArrayList<>(); + assertEquals(0, combinator.difference(item, entries)); + } + + @Test + public void testDifferenceDifferentSize() { + Map item = new HashMap<>(); + List entries = new ArrayList<>(); + assertTrue(entries.add(new IndexEntry("mode", "type", "object", "file"))); + assertEquals(-1, combinator.difference(item, entries)); + } + + @Test + public void testDifferenceNoDifference() { + Map items = new HashMap<>(); + List entries = new ArrayList<>(); + ObjectId sha1 = ObjectId.fromString("1c2a9e6194e6ede0805cda4c9ccc7e373e835414"); + IndexEntry indexEntry1 = new IndexEntry("mode-1", "type-1", sha1.getName(), "file-1"); + assertTrue("Failed to add indexEntry1 to entries", entries.add(indexEntry1)); + Revision revision = new Revision(sha1); + assertNull("items[indexEntry1] had existing value", items.put(indexEntry1, revision)); + assertEquals("entries and items[entries] don't match", 0, combinator.difference(items, entries)); + } + + @Test + public void testDifferenceOneDifference() { + Map items = new HashMap<>(); + List entries = new ArrayList<>(); + ObjectId sha1 = ObjectId.fromString("1c2a9e6194e6ede0805cda4c9ccc7e373e835414"); + String fileName = "fileName"; + IndexEntry indexEntry1 = new IndexEntry("mode-1", "type-1", sha1.getName(), fileName); + assertTrue("Failed to add indexEntry1 to entries", entries.add(indexEntry1)); + ObjectId sha2 = ObjectId.fromString("54094393c170c94d330b1ae52101922092b0abd2"); + Revision revision = new Revision(sha2); + IndexEntry indexEntry2 = new IndexEntry("mode-2", "type-2", sha2.getName(), fileName); + assertNull("items[indexEntry2] had existing value", items.put(indexEntry2, revision)); + assertEquals("entries and items[entries] wrong diff count", 1, combinator.difference(items, entries)); + } +} diff --git a/src/test/java/hudson/plugins/git/SubmoduleConfigTest.java b/src/test/java/hudson/plugins/git/SubmoduleConfigTest.java new file mode 100644 index 0000000000..09c123eab5 --- /dev/null +++ b/src/test/java/hudson/plugins/git/SubmoduleConfigTest.java @@ -0,0 +1,244 @@ +/* + * The MIT License + * + * Copyright 2017-2019 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class SubmoduleConfigTest { + + private SubmoduleConfig config = new SubmoduleConfig(); + private SubmoduleConfig configWithBranchArray; + private SubmoduleConfig configWithBranchList; + + private static final String SHA1 = "beaddeedfeedcededeafcadebadeabadedfeedad"; + private static final ObjectId ID = ObjectId.fromString(SHA1); + private static final ObjectId ID2 = ObjectId.fromString(SHA1.replace('a', 'e')); + + private final String[] branchNames = {"master", "comma,chameleon", "develop"}; + + private final Revision emptyRevision; + private final Revision noBranchesRevision; + private final Revision multipleBranchesRevision; + + private final Branch masterBranch; + private final Branch masterAliasBranch; + private final Branch developBranch; + + private final List branchNameList = new ArrayList<>(); + + public SubmoduleConfigTest() { + List emptyBranchList = new ArrayList<>(); + emptyRevision = new Revision(ID, emptyBranchList); + masterBranch = new Branch("master", ID); + masterAliasBranch = new Branch("masterAlias", ID); + developBranch = new Branch("develop", ID2); + List branchList = new ArrayList<>(); + branchList.add(masterBranch); + branchList.add(masterAliasBranch); + branchList.add(developBranch); + noBranchesRevision = new Revision(ID); + multipleBranchesRevision = new Revision(ID, branchList); + branchNameList.add("master"); + branchNameList.add("masterAlias"); + branchNameList.add("develop"); + } + + @Before + public void setUp() { + config = new SubmoduleConfig(); + configWithBranchArray = new SubmoduleConfig("submodule-branch-names-from-array", branchNames); + configWithBranchList = new SubmoduleConfig("submodule-branch-names-from-list", branchNameList); + } + + @Test + public void testGetSubmoduleName() { + assertThat(config.getSubmoduleName(), is(nullValue())); + } + + @Test + public void testSetSubmoduleName() { + String name = "name-of-submodule"; + config.setSubmoduleName(name); + assertThat(config.getSubmoduleName(), is(name)); + name = "another-submodule"; + config.setSubmoduleName(name); + assertThat(config.getSubmoduleName(), is(name)); + } + + @Test(expected = NullPointerException.class) + public void testGetBranches() { + config.getBranches(); + } + + public void testGetBranchesFromArray() { + assertThat(configWithBranchArray.getBranches(), is(branchNames)); + } + + public void testGetBranchesFromList() { + assertThat(configWithBranchList.getBranches(), is(branchNames)); + } + + @Test + public void testSetBranches() { + config.setBranches(branchNames); + assertThat(config.getBranches(), is(branchNames)); + String[] newBranchNames = Arrays.copyOf(branchNames, branchNames.length); + newBranchNames[0] = "new-master"; + config.setBranches(newBranchNames); + assertThat(config.getBranches(), is(newBranchNames)); + } + + @Test(expected = NullPointerException.class) + public void testGetBranchesStringNPE() { + config.getBranchesString(); + } + + @Test + public void testGetBranchesString() { + config.setBranches(branchNames); + assertThat(config.getBranchesString(), is("master,comma,chameleon,develop")); + } + + @Test + public void testGetBranchesStringFromList() { + assertThat(configWithBranchList.getBranchesString(), is("master,masterAlias,develop")); + configWithBranchList.setBranches(branchNames); + assertThat(configWithBranchList.getBranchesString(), is("master,comma,chameleon,develop")); + } + + @Test + public void testGetBranchesStringFromArray() { + assertThat(configWithBranchArray.getBranchesString(), is("master,comma,chameleon,develop")); + configWithBranchArray.setBranches(branchNames); + assertThat(configWithBranchArray.getBranchesString(), is("master,comma,chameleon,develop")); + } + + @Test + public void testRevisionMatchesInterestNoBranches() { + assertFalse(config.revisionMatchesInterest(noBranchesRevision)); + assertFalse(configWithBranchList.revisionMatchesInterest(noBranchesRevision)); + assertFalse(configWithBranchArray.revisionMatchesInterest(noBranchesRevision)); + } + + @Test + public void testRevisionMatchesInterestEmptyBranchList() { + assertFalse(config.revisionMatchesInterest(emptyRevision)); + assertFalse(configWithBranchList.revisionMatchesInterest(emptyRevision)); + assertFalse(configWithBranchArray.revisionMatchesInterest(emptyRevision)); + } + + @Test(expected = NullPointerException.class) + public void testRevisionMatchesInterestNPE() { + config.revisionMatchesInterest(multipleBranchesRevision); + } + + @Test + public void testRevisionMatchesInterestMasterOnly() { + String[] masterOnly = {"master"}; + config.setBranches(masterOnly); + assertTrue(config.revisionMatchesInterest(multipleBranchesRevision)); + + assertFalse(configWithBranchList.revisionMatchesInterest(multipleBranchesRevision)); + configWithBranchList.setBranches(masterOnly); + assertTrue(configWithBranchList.revisionMatchesInterest(multipleBranchesRevision)); + + assertFalse(configWithBranchArray.revisionMatchesInterest(multipleBranchesRevision)); + configWithBranchArray.setBranches(masterOnly); + assertTrue(configWithBranchArray.revisionMatchesInterest(multipleBranchesRevision)); + } + + @Test + public void testRevisionMatchesInterestAlias() { + String[] aliasName = {"masterAlias"}; + config.setBranches(aliasName); + assertTrue(config.revisionMatchesInterest(multipleBranchesRevision)); + + assertFalse(configWithBranchList.revisionMatchesInterest(multipleBranchesRevision)); + configWithBranchList.setBranches(aliasName); + assertTrue(configWithBranchList.revisionMatchesInterest(multipleBranchesRevision)); + + assertFalse(configWithBranchArray.revisionMatchesInterest(multipleBranchesRevision)); + configWithBranchArray.setBranches(aliasName); + assertTrue(configWithBranchArray.revisionMatchesInterest(multipleBranchesRevision)); + } + + @Test + public void testRevisionMatchesInterest() { + String[] masterDevelop = {"master", "develop"}; + config.setBranches(masterDevelop); + assertFalse(config.revisionMatchesInterest(multipleBranchesRevision)); + assertFalse(configWithBranchList.revisionMatchesInterest(multipleBranchesRevision)); + assertFalse(configWithBranchArray.revisionMatchesInterest(multipleBranchesRevision)); + } + + @Test + public void testBranchMatchesInterest() { + String[] masterOnly = {"master"}; + config.setBranches(masterOnly); + assertTrue(config.branchMatchesInterest(masterBranch)); + assertFalse(config.branchMatchesInterest(masterAliasBranch)); + assertFalse(config.branchMatchesInterest(developBranch)); + + assertFalse(configWithBranchList.branchMatchesInterest(masterBranch)); + assertFalse(configWithBranchList.branchMatchesInterest(masterAliasBranch)); + assertFalse(configWithBranchList.branchMatchesInterest(developBranch)); + configWithBranchList.setBranches(masterOnly); + assertTrue(configWithBranchList.branchMatchesInterest(masterBranch)); + assertFalse(configWithBranchList.branchMatchesInterest(masterAliasBranch)); + assertFalse(configWithBranchList.branchMatchesInterest(developBranch)); + } + + @Test + public void testBranchMatchesInterestWithRegex() { + String[] masterOnlyRegex = {"m.st.r"}; + config.setBranches(masterOnlyRegex); + assertTrue(config.branchMatchesInterest(masterBranch)); + assertFalse(config.branchMatchesInterest(masterAliasBranch)); + assertFalse(config.branchMatchesInterest(developBranch)); + } + + @Test + public void testBranchMatchesInterestMasterDevelop() { + String[] masterDevelop = {"master", "develop"}; + config.setBranches(masterDevelop); + assertFalse(config.branchMatchesInterest(masterBranch)); + assertFalse(config.branchMatchesInterest(masterAliasBranch)); + assertFalse(config.branchMatchesInterest(developBranch)); + } + + @Test + public void testBranchMatchesInterestCommaInBranchName() { + config.setBranches(branchNames); + assertFalse(config.branchMatchesInterest(masterBranch)); + assertFalse(config.branchMatchesInterest(masterAliasBranch)); + assertFalse(config.branchMatchesInterest(developBranch)); + } +} diff --git a/src/test/java/hudson/plugins/git/TestBranchSpec.java b/src/test/java/hudson/plugins/git/TestBranchSpec.java deleted file mode 100644 index 813e5442ea..0000000000 --- a/src/test/java/hudson/plugins/git/TestBranchSpec.java +++ /dev/null @@ -1,149 +0,0 @@ -package hudson.plugins.git; - -import hudson.EnvVars; -import java.util.HashMap; - -import junit.framework.Assert; -import junit.framework.TestCase; - -public class TestBranchSpec extends TestCase { - public void testMatch() { - BranchSpec l = new BranchSpec("master"); - Assert.assertTrue(l.matches("origin/master")); - Assert.assertFalse(l.matches("origin/something/master")); - Assert.assertFalse(l.matches("master")); - Assert.assertFalse(l.matches("dev")); - - - BranchSpec est = new BranchSpec("origin/*/dev"); - - Assert.assertFalse(est.matches("origintestdev")); - Assert.assertTrue(est.matches("origin/test/dev")); - Assert.assertFalse(est.matches("origin/test/release")); - Assert.assertFalse(est.matches("origin/test/somthing/release")); - - BranchSpec s = new BranchSpec("origin/*"); - - Assert.assertTrue(s.matches("origin/master")); - - BranchSpec m = new BranchSpec("**/magnayn/*"); - - Assert.assertTrue(m.matches("origin/magnayn/b1")); - Assert.assertTrue(m.matches("remote/origin/magnayn/b1")); - Assert.assertTrue(m.matches("remotes/origin/magnayn/b1")); - - BranchSpec n = new BranchSpec("*/my.branch/*"); - - Assert.assertTrue(n.matches("origin/my.branch/b1")); - Assert.assertFalse(n.matches("origin/my-branch/b1")); - Assert.assertFalse(n.matches("remote/origin/my.branch/b1")); - Assert.assertFalse(n.matches("remotes/origin/my.branch/b1")); - - BranchSpec o = new BranchSpec("**"); - - Assert.assertTrue(o.matches("origin/my.branch/b1")); - Assert.assertTrue(o.matches("origin/my-branch/b1")); - Assert.assertTrue(o.matches("remote/origin/my.branch/b1")); - Assert.assertTrue(o.matches("remotes/origin/my.branch/b1")); - - BranchSpec p = new BranchSpec("*"); - - Assert.assertTrue(p.matches("origin/x")); - Assert.assertFalse(p.matches("origin/my-branch/b1")); - } - - public void testMatchEnv() { - HashMap envMap = new HashMap(); - envMap.put("master", "master"); - envMap.put("origin", "origin"); - envMap.put("dev", "dev"); - envMap.put("magnayn", "magnayn"); - envMap.put("mybranch", "my.branch"); - envMap.put("anyLong", "**"); - envMap.put("anyShort", "*"); - EnvVars env = new EnvVars(envMap); - - BranchSpec l = new BranchSpec("${master}"); - Assert.assertTrue(l.matches("origin/master", env)); - Assert.assertFalse(l.matches("origin/something/master", env)); - Assert.assertFalse(l.matches("master", env)); - Assert.assertFalse(l.matches("dev", env)); - - - BranchSpec est = new BranchSpec("${origin}/*/${dev}"); - - Assert.assertFalse(est.matches("origintestdev", env)); - Assert.assertTrue(est.matches("origin/test/dev", env)); - Assert.assertFalse(est.matches("origin/test/release", env)); - Assert.assertFalse(est.matches("origin/test/somthing/release", env)); - - BranchSpec s = new BranchSpec("${origin}/*"); - - Assert.assertTrue(s.matches("origin/master", env)); - - BranchSpec m = new BranchSpec("**/${magnayn}/*"); - - Assert.assertTrue(m.matches("origin/magnayn/b1", env)); - Assert.assertTrue(m.matches("remote/origin/magnayn/b1", env)); - - BranchSpec n = new BranchSpec("*/${mybranch}/*"); - - Assert.assertTrue(n.matches("origin/my.branch/b1", env)); - Assert.assertFalse(n.matches("origin/my-branch/b1", env)); - Assert.assertFalse(n.matches("remote/origin/my.branch/b1", env)); - - BranchSpec o = new BranchSpec("${anyLong}"); - - Assert.assertTrue(o.matches("origin/my.branch/b1", env)); - Assert.assertTrue(o.matches("origin/my-branch/b1", env)); - Assert.assertTrue(o.matches("remote/origin/my.branch/b1", env)); - - BranchSpec p = new BranchSpec("${anyShort}"); - - Assert.assertTrue(p.matches("origin/x", env)); - Assert.assertFalse(p.matches("origin/my-branch/b1", env)); - } - - public void testEmptyName() { - BranchSpec branchSpec = new BranchSpec(""); - assertEquals("**",branchSpec.getName()); - } - - public void testNullName() { - boolean correctExceptionThrown = false; - try { - BranchSpec branchSpec = new BranchSpec(null); - } catch (IllegalArgumentException e) { - correctExceptionThrown = true; - } - assertTrue(correctExceptionThrown); - } - - public void testNameTrimming() { - BranchSpec branchSpec = new BranchSpec(" master "); - assertEquals("master",branchSpec.getName()); - branchSpec.setName(" other "); - assertEquals("other",branchSpec.getName()); - } - - public void testUsesRefsHeads() { - BranchSpec m = new BranchSpec("refs/heads/j*n*"); - assertTrue(m.matches("refs/heads/jenkins")); - assertTrue(m.matches("refs/heads/jane")); - assertTrue(m.matches("refs/heads/jones")); - - assertFalse(m.matches("origin/jenkins")); - assertFalse(m.matches("remote/origin/jane")); - } - - public void testUsesJavaPatternDirectlyIfPrefixedWithColon() { - BranchSpec m = new BranchSpec(":^(?!(origin/prefix)).*"); - assertTrue(m.matches("origin")); - assertTrue(m.matches("origin/master")); - assertTrue(m.matches("origin/feature")); - - assertFalse(m.matches("origin/prefix_123")); - assertFalse(m.matches("origin/prefix")); - assertFalse(m.matches("origin/prefix-abc")); - } -} diff --git a/src/test/java/hudson/plugins/git/TestGitRepo.java b/src/test/java/hudson/plugins/git/TestGitRepo.java index affe8764b8..647d163d9e 100644 --- a/src/test/java/hudson/plugins/git/TestGitRepo.java +++ b/src/test/java/hudson/plugins/git/TestGitRepo.java @@ -1,5 +1,6 @@ package hudson.plugins.git; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import hudson.EnvVars; import hudson.FilePath; import hudson.model.TaskListener; @@ -12,11 +13,9 @@ import java.util.ArrayList; import java.util.List; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.PersonIdent; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; -import org.jvnet.hudson.test.HudsonTestCase; public class TestGitRepo { protected String name; // The name of this repository. @@ -29,19 +28,16 @@ public class TestGitRepo { public FilePath gitDirPath; // was "workspace" public GitClient git; - private EnvVars envVars; - public final PersonIdent johnDoe = new PersonIdent("John Doe", "john@doe.com"); public final PersonIdent janeDoe = new PersonIdent("Jane Doe", "jane@doe.com"); - public TestGitRepo(String name, HudsonTestCase forTest, TaskListener listener) - throws IOException, InterruptedException { + public TestGitRepo(String name, File tmpDir, TaskListener listener) throws IOException, InterruptedException { this.name = name; this.listener = listener; - envVars = new EnvVars(); + EnvVars envVars = new EnvVars(); - gitDir = forTest.createTmpDir(); + gitDir = tmpDir; User john = User.get(johnDoe.getName(), true); UserProperty johnsMailerProperty = new Mailer.UserProperty(johnDoe.getEmailAddress()); john.addProperty(johnsMailerProperty); @@ -58,25 +54,63 @@ public TestGitRepo(String name, HudsonTestCase forTest, TaskListener listener) git.init(); } - public void commit(final String fileName, final PersonIdent committer, final String message) + /** + * Creates a commit in current repo. + * @param fileName relative path to the file to be committed with default content + * @param committer author and committer of this commit + * @param message commit message + * @return SHA1 of latest commit + * @throws GitException on git error + * @throws InterruptedException when interrupted + */ + public String commit(final String fileName, final PersonIdent committer, final String message) throws GitException, InterruptedException { - commit(fileName, fileName, committer, committer, message); + return commit(fileName, fileName, committer, committer, message); } - public void commit(final String fileName, final PersonIdent author, final PersonIdent committer, final String message) - + /** + * Creates a commit in current repo. + * @param fileName relative path to the file to be committed with default content + * @param author author of the commit + * @param committer committer of this commit + * @param message commit message + * @return SHA1 of latest commit + * @throws GitException on git error + * @throws InterruptedException when interrupted + */ + public String commit(final String fileName, final PersonIdent author, final PersonIdent committer, final String message) throws GitException, InterruptedException { - commit(fileName, fileName, author, committer, message); + return commit(fileName, fileName, author, committer, message); } - public void commit(final String fileName, final String fileContent, final PersonIdent committer, final String message) - + /** + * Creates a commit in current repo. + * @param fileName relative path to the file to be committed with the given content + * @param fileContent content of the commit + * @param committer author and committer of this commit + * @param message commit message + * @return SHA1 of latest commit + * @throws GitException on git error + * @throws InterruptedException when interrupted + */ + public String commit(final String fileName, final String fileContent, final PersonIdent committer, final String message) throws GitException, InterruptedException { - commit(fileName, fileContent, committer, committer, message); + return commit(fileName, fileContent, committer, committer, message); } - public void commit(final String fileName, final String fileContent, final PersonIdent author, final PersonIdent committer, - final String message) throws GitException, InterruptedException { + /** + * Creates a commit in current repo. + * @param fileName relative path to the file to be committed with the given content + * @param fileContent content of the commit + * @param author author of the commit + * @param committer committer of this commit + * @param message commit message + * @return SHA1 of latest commit + * @throws GitException on git error + * @throws InterruptedException when interrupted + */ + public String commit(final String fileName, final String fileContent, final PersonIdent author, final PersonIdent committer, + final String message) throws GitException, InterruptedException { FilePath file = gitDirPath.child(fileName); try { file.write(fileContent, null); @@ -84,12 +118,24 @@ public void commit(final String fileName, final String fileContent, final Person throw new GitException("unable to write file", e); } git.add(fileName); - git.commit(message, author, committer); + git.setAuthor(author); + git.setCommitter(committer); + git.commit(message); + return git.revParse("HEAD").getName(); + } + + public void tag(String tagName, String comment) throws GitException, InterruptedException { + git.tag(tagName, comment); } public List remoteConfigs() throws IOException { - List list = new ArrayList(); - list.add(new UserRemoteConfig(gitDir.getAbsolutePath(), "origin", "", null)); + return remoteConfigs(null); + } + + List remoteConfigs(StandardCredentials credentials) { + String credentialsId = credentials == null ? null : credentials.getId(); + List list = new ArrayList<>(); + list.add(new UserRemoteConfig(gitDir.getAbsolutePath(), "origin", "", credentialsId)); return list; } } diff --git a/src/test/java/hudson/plugins/git/UserMergeOptionsTest.java b/src/test/java/hudson/plugins/git/UserMergeOptionsTest.java new file mode 100644 index 0000000000..1a5759cd8d --- /dev/null +++ b/src/test/java/hudson/plugins/git/UserMergeOptionsTest.java @@ -0,0 +1,232 @@ +package hudson.plugins.git; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.jenkinsci.plugins.structs.describable.DescribableModel; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +@RunWith(Parameterized.class) +public class UserMergeOptionsTest { + + public static @ClassRule JenkinsRule r = new JenkinsRule(); + + private final UserMergeOptions options; + private final UserMergeOptions deprecatedOptions; + + private final String expectedMergeRemote; + private final String expectedMergeTarget; + private final MergeCommand.Strategy expectedMergeStrategy; + private final MergeCommand.GitPluginFastForwardMode expectedFastForwardMode; + + public UserMergeOptionsTest( + String mergeRemote, + String mergeTarget, + MergeCommand.Strategy mergeStrategy, + MergeCommand.GitPluginFastForwardMode fastForwardMode) { + this.expectedMergeRemote = mergeRemote; + this.expectedMergeTarget = mergeTarget; + this.expectedMergeStrategy = mergeStrategy; + this.expectedFastForwardMode = fastForwardMode; + options = new UserMergeOptions( + mergeRemote, + mergeTarget, + mergeStrategy == null ? null : mergeStrategy.toString(), + fastForwardMode); + deprecatedOptions = new UserMergeOptions( + mergeRemote, + mergeTarget, + mergeStrategy == null ? null : mergeStrategy.toString()); + } + + @Parameterized.Parameters(name = "{0}+{1}+{2}+{3}") + public static Collection mergeOptionVariants() { + List mergeOptions = new ArrayList<>(); + String[] remotes = new String[]{null, "src_remote"}; + String[] targets = new String[]{null, "dst_remote"}; + MergeCommand.Strategy[] mergeStrategies = new MergeCommand.Strategy[]{ + null, + MergeCommand.Strategy.DEFAULT, + MergeCommand.Strategy.OCTOPUS, + MergeCommand.Strategy.OURS, + MergeCommand.Strategy.RECURSIVE, + MergeCommand.Strategy.RESOLVE, + MergeCommand.Strategy.SUBTREE + }; + MergeCommand.GitPluginFastForwardMode[] fastForwardModes = new MergeCommand.GitPluginFastForwardMode[]{ + null, + MergeCommand.GitPluginFastForwardMode.FF, + MergeCommand.GitPluginFastForwardMode.FF_ONLY, + MergeCommand.GitPluginFastForwardMode.NO_FF + }; + for (String remote : remotes) { + for (String target : targets) { + for (MergeCommand.Strategy strategy : mergeStrategies) { + for (MergeCommand.GitPluginFastForwardMode mode : fastForwardModes) { + Object[] mergeOption = {remote, target, strategy, mode}; + mergeOptions.add(mergeOption); + } + } + } + } + return mergeOptions; + } + + @Test + public void testGetMergeRemote() { + assertEquals(expectedMergeRemote, options.getMergeRemote()); + } + + @Test + public void testGetMergeTarget() { + assertEquals(expectedMergeTarget, options.getMergeTarget()); + } + + @Test + public void testGetRef() { + assertEquals(expectedMergeRemote + "/" + expectedMergeTarget, options.getRef()); + } + + @Test + public void testGetMergeStrategy() { + assertEquals(expectedMergeStrategy == null ? MergeCommand.Strategy.DEFAULT : expectedMergeStrategy, options.getMergeStrategy()); + } + + @Test + public void testGetFastForwardMode() { + assertEquals(expectedFastForwardMode == null ? MergeCommand.GitPluginFastForwardMode.FF : expectedFastForwardMode, options.getFastForwardMode()); + } + + @Test + public void testToString() { + final String expected = "UserMergeOptions{" + + "mergeRemote='" + expectedMergeRemote + "', " + + "mergeTarget='" + expectedMergeTarget + "', " + + "mergeStrategy='" + (expectedMergeStrategy == null ? MergeCommand.Strategy.DEFAULT : expectedMergeStrategy).name() + "', " + + "fastForwardMode='" + (expectedFastForwardMode == null ? MergeCommand.GitPluginFastForwardMode.FF : expectedFastForwardMode).name() + "'" + + '}'; + assertEquals(expected, options.toString()); + } + + @Test + public void testEqualsSymmetric() { + UserMergeOptions expected = new UserMergeOptions( + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + assertEquals(expected, options); + assertEquals(options, expected); + } + + @Test + public void testEqualsReflexive() { + UserMergeOptions expected = new UserMergeOptions( + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + /* reflexive */ + assertEquals(options, options); + assertEquals(expected, expected); + } + + @Test + public void testEqualsTransitive() { + UserMergeOptions expected = new UserMergeOptions( + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + UserMergeOptions expected1 = new UserMergeOptions( + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + assertEquals(expected, expected1); + assertEquals(expected1, options); + assertEquals(expected, options); + } + + @Test + public void testEqualsDeprecatedConstructor() { + if (this.expectedFastForwardMode == MergeCommand.GitPluginFastForwardMode.FF) { + assertEquals(options, deprecatedOptions); + } else { + assertNotEquals(options, deprecatedOptions); + } + } + + @Test + public void testNotEquals() { + UserMergeOptions notExpected1 = new UserMergeOptions( + "x" + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + assertNotEquals(notExpected1, options); + UserMergeOptions notExpected2 = new UserMergeOptions( + this.expectedMergeRemote, + "y" + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + assertNotEquals(notExpected2, options); + assertNotEquals(options, "A different data type"); + } + + @Test + public void testHashCode() { + UserMergeOptions expected = new UserMergeOptions( + this.expectedMergeRemote, + this.expectedMergeTarget, + this.expectedMergeStrategy == null ? null : this.expectedMergeStrategy.toString(), + this.expectedFastForwardMode); + assertEquals(expected, options); + assertEquals(expected.hashCode(), options.hashCode()); + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(UserMergeOptions.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } + + @Issue({"JENKINS-51638", "JENKINS-34070"}) + @Test + public void mergeStrategyCase() throws Exception { + Map args = new HashMap<>(); + if (expectedMergeTarget != null) { + args.put("mergeTarget", expectedMergeTarget); + } + if (expectedMergeRemote != null) { + args.put("mergeRemote", expectedMergeRemote); + } + if (expectedMergeStrategy != null) { + // Recommend syntax as of JENKINS-34070: + args.put("mergeStrategy", expectedMergeStrategy.name()); + } + if (expectedFastForwardMode != null) { + args.put("fastForwardMode", expectedFastForwardMode.name()); + } + assertEquals(options, new DescribableModel<>(UserMergeOptions.class).instantiate(args)); + if (expectedMergeStrategy != null) { + // Historically accepted lowercase strings here: + args.put("mergeStrategy", expectedMergeStrategy.toString()); + assertEquals(options, new DescribableModel<>(UserMergeOptions.class).instantiate(args)); + } + } + +} diff --git a/src/test/java/hudson/plugins/git/UserRemoteConfigTest.java b/src/test/java/hudson/plugins/git/UserRemoteConfigTest.java new file mode 100644 index 0000000000..30ddff3757 --- /dev/null +++ b/src/test/java/hudson/plugins/git/UserRemoteConfigTest.java @@ -0,0 +1,64 @@ +package hudson.plugins.git; + +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import com.google.common.collect.Sets; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.User; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +public class UserRemoteConfigTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Issue("JENKINS-38048") + @Test + public void credentialsDropdown() throws Exception { + SystemCredentialsProvider.getInstance().getCredentials().add(new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "mycreds", null, "jenkins", "s3cr3t")); + SystemCredentialsProvider.getInstance().save(); + FreeStyleProject p1 = r.createFreeStyleProject("p1"); + FreeStyleProject p2 = r.createFreeStyleProject("p2"); + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). + grant(Jenkins.ADMINISTER).everywhere().to("admin"). + grant(Jenkins.READ, Item.READ).everywhere().to("dev"). + grant(Item.EXTENDED_READ).onItems(p1).to("dev")); + assertCredentials(p1, null, "dev", "", "mycreds"); + assertCredentials(p2, null, "dev", ""); + assertCredentials(p1, null, "admin", "", "mycreds"); + assertCredentials(p2, null, "admin", "", "mycreds"); + assertCredentials(p1, "othercreds", "dev", "", "mycreds", "othercreds"); + assertCredentials(null, null, "dev", ""); + assertCredentials(null, null, "admin", "", "mycreds"); + assertCredentials(null, "othercreds", "admin", "", "mycreds", "othercreds"); + } + + private void assertCredentials(@CheckForNull final Item project, @CheckForNull final String currentCredentialsId, @Nonnull String user, @Nonnull String... expectedCredentialsIds) { + final Set actual = new TreeSet<>(); // for purposes of this test we do not care about order (though StandardListBoxModel does define some) + ACL.impersonate(User.get(user).impersonate(), () -> { + for (ListBoxModel.Option option : r.jenkins.getDescriptorByType(UserRemoteConfig.DescriptorImpl.class). + doFillCredentialsIdItems(project, "http://wherever.jenkins.io/", currentCredentialsId)) { + actual.add(option.value); + } + }); + assertEquals("expected completions on " + project + " as " + user + " starting with " + currentCredentialsId, + Sets.newTreeSet(Arrays.asList(expectedCredentialsIds)), actual); + } + +} diff --git a/src/test/java/hudson/plugins/git/browser/AssemblaWebTest.java b/src/test/java/hudson/plugins/git/browser/AssemblaWebTest.java new file mode 100644 index 0000000000..dcec85f797 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/AssemblaWebTest.java @@ -0,0 +1,75 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.scm.EditType; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class AssemblaWebTest { + + private final String repoUrl = "http://assembla.example.com/"; + + private final boolean useAuthorName; + private final GitChangeSetSample sample; + + public AssemblaWebTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorName() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new AssemblaWeb(repoUrl)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "commits/" + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + AssemblaWeb assemblaWeb = new AssemblaWeb(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = assemblaWeb.getDiffLink(path); + EditType editType = path.getEditType(); + URL expectedDiffLink = new URL(repoUrl + "commits/" + sample.id); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + AssemblaWeb assemblaWeb = new AssemblaWeb(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = assemblaWeb.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedFileLink = new URL(repoUrl + "nodes/" + sample.id + path.getPath()); + } else if (editType == EditType.DELETE) { + expectedFileLink = new URL(repoUrl + "nodes/" + sample.parent + path.getPath()); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } + +} diff --git a/src/test/java/hudson/plugins/git/browser/BitbucketWebTest.java b/src/test/java/hudson/plugins/git/browser/BitbucketWebTest.java index b25a0213e0..130924f9b1 100644 --- a/src/test/java/hudson/plugins/git/browser/BitbucketWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/BitbucketWebTest.java @@ -4,64 +4,50 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; -import junit.framework.TestCase; -import org.xml.sax.SAXException; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author mattsemar - * */ -public class BitbucketWebTest extends TestCase { +public class BitbucketWebTest { - /** - * - */ private static final String BITBUCKET_URL = "http://bitbucket.org/USER/REPO"; private final BitbucketWeb bitbucketWeb = new BitbucketWeb(BITBUCKET_URL); - /** - * Test method for {@link BitbucketWeb#getUrl()}. - * @throws java.net.MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(bitbucketWeb.getUrl()), BITBUCKET_URL + "/"); } - /** - * Test method for {@link BitbucketWeb#getUrl()}. - * @throws java.net.MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new BitbucketWeb(BITBUCKET_URL + "/").getUrl()), BITBUCKET_URL + "/"); } - /** - * Test method for {@link BitbucketWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws org.xml.sax.SAXException - * @throws java.io.IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = bitbucketWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(BITBUCKET_URL + "/commits/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link BitbucketWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws org.xml.sax.SAXException - * @throws java.io.IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final String path1Str = "src/main/java/hudson/plugins/git/browser/GithubWeb.java"; final Path path1 = pathMap.get(path1Str); @@ -76,51 +62,38 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertNull("Do not return a diff link for added files.", bitbucketWeb.getDiffLink(path3)); } - /** - * Test method for {@link GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws org.xml.sax.SAXException - * @throws java.io.IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = bitbucketWeb.getFileLink(path); assertEquals(BITBUCKET_URL + "/history/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); } - /** - * Test method for {@link BitbucketWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws org.xml.sax.SAXException - * @throws java.io.IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = bitbucketWeb.getFileLink(path); assertEquals(BITBUCKET_URL + "/history/bar", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(BitbucketWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? "Default" : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(BitbucketWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws java.io.IOException - * @throws org.xml.sax.SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/BrowserChooserTest.java b/src/test/java/hudson/plugins/git/browser/BrowserChooserTest.java deleted file mode 100644 index b659f39d9e..0000000000 --- a/src/test/java/hudson/plugins/git/browser/BrowserChooserTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright 2010 Mirko Friedenhagen - */ - -package hudson.plugins.git.browser; - -import hudson.util.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; - -import javax.servlet.http.HttpServletRequest; - -import junit.framework.TestCase; -import net.sf.json.JSONObject; -import net.sf.json.JSONSerializer; - -import org.kohsuke.stapler.RequestImpl; -import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.WebApp; -import org.mockito.Mockito; - -/** - * - * This class tests switching between the different browser implementation. - * - * @author mfriedenhagen - */ -public class BrowserChooserTest extends TestCase { - - private final Stapler stapler = Mockito.mock(Stapler.class); - - private final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class); - - @SuppressWarnings("unchecked") - private final StaplerRequest staplerRequest = new RequestImpl(stapler, servletRequest, Collections.EMPTY_LIST, null); - - { - final WebApp webApp = Mockito.mock(WebApp.class); - Mockito.when(webApp.getClassLoader()).thenReturn(this.getClass().getClassLoader()); - Mockito.when(stapler.getWebApp()).thenReturn(webApp); - // TODO need to mock also WebApp.bindInterceptors and MetaClass.getPostConstructMethods to pass in 1.540; would probably be easier to not use Mockito for all this - } - - public void testRedmineWeb() throws IOException { - testExistingBrowser(RedmineWeb.class); - } - - public void testGitoriousWeb() throws IOException { - testExistingBrowser(GitoriousWeb.class); - } - - public void testGithubWeb() throws IOException { - testExistingBrowser(GithubWeb.class); - } - - public void testGitWeb() throws IOException { - testExistingBrowser(GitWeb.class); - } - - public void testNonExistingBrowser() throws IOException { - final JSONObject json = readJson(); - try { - createBrowserFromJson(json); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - assertSame(e.getCause().getCause().getClass(), ClassNotFoundException.class); - } - } - - /** - * @param browserClass - * @throws IOException - */ - void testExistingBrowser(final Class browserClass) throws IOException { - final JSONObject json = readJson(browserClass); - assertSame(browserClass, createBrowserFromJson(json).getClass()); - } - - /** - * @param json - * @return - */ - GitRepositoryBrowser createBrowserFromJson(final JSONObject json) { - GitRepositoryBrowser browser = staplerRequest.bindJSON(GitRepositoryBrowser.class, - json.getJSONObject("browser")); - return browser; - } - - /** - * Reads the request data from file scm.json and replaces the invalid browser class in the JSONObject with the class - * specified as parameter. - * - * @param browserClass - * @return - * @throws IOException - */ - JSONObject readJson(Class browserClass) throws IOException { - final JSONObject json = readJson(); - json.getJSONObject("browser").element("stapler-class", browserClass.getName()); - return json; - } - - /** - * Reads the request data from file scm.json. - * - * @return - * @throws IOException - */ - JSONObject readJson() throws IOException { - final InputStream stream = this.getClass().getResourceAsStream("scm.json"); - final String scmString; - try { - scmString = IOUtils.toString(stream); - } finally { - stream.close(); - } - final JSONObject json = (JSONObject) JSONSerializer.toJSON(scmString); - return json; - } -} diff --git a/src/test/java/hudson/plugins/git/browser/CGitTest.java b/src/test/java/hudson/plugins/git/browser/CGitTest.java new file mode 100644 index 0000000000..3469f6d91f --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/CGitTest.java @@ -0,0 +1,83 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.scm.EditType; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class CGitTest { + + private final String repoUrl = "http://cgit.example.com/"; + + private final boolean useAuthorName; + private final GitChangeSetSample sample; + + public CGitTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorName() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new CGit(repoUrl)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "commit/?id=" + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + CGit cgit = new CGit(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = cgit.getDiffLink(path); + EditType editType = path.getEditType(); + URL expectedDiffLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedDiffLink = new URL(repoUrl + "diff/" + path.getPath() + "?id=" + sample.id); + } else if (editType == EditType.DELETE) { + // Surprising that the DELETE EditType uses sample.id and not sample.parent ??? + expectedDiffLink = new URL(repoUrl + "diff/" + path.getPath() + "?id=" + sample.id); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + CGit cgit = new CGit(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = cgit.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedFileLink = new URL(repoUrl + "tree/" + path.getPath() + "?id=" + sample.id); + } else if (editType == EditType.DELETE) { + expectedFileLink = new URL(repoUrl + "tree/" + path.getPath() + "?id=" + sample.parent); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } + +} diff --git a/src/test/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowserTest.java b/src/test/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowserTest.java new file mode 100644 index 0000000000..3d439a2eff --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/FisheyeGitRepositoryBrowserTest.java @@ -0,0 +1,87 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.scm.EditType; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class FisheyeGitRepositoryBrowserTest { + + private static final String projectName = "fisheyeProjectName"; + + private final String repoUrl; + private final String repoUrlNoTrailingSlash; + private final boolean useAuthorName; + private final GitChangeSetSample sample; + + public FisheyeGitRepositoryBrowserTest(String useAuthorName, String repoUrl) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + this.repoUrl = repoUrl; + this.repoUrlNoTrailingSlash = this.repoUrl.endsWith("/") ? repoUrl.substring(0, repoUrl.length() - 1) : repoUrl; + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}-{1}") + public static Collection permuteAuthorNameAndRepoUrl() { + List values = new ArrayList<>(); + String fisheyeUrl = "http://fisheye.example.com/site/browse/" + projectName; + String[] allowedUrls = {fisheyeUrl, fisheyeUrl + "/"}; + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + for (String repoUrl : allowedUrls) { + Object[] combination = {authorName, repoUrl}; + values.add(combination); + } + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new FisheyeGitRepositoryBrowser(repoUrl)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrlNoTrailingSlash.replace("browse", "changelog") + "?cs=" + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + FisheyeGitRepositoryBrowser fisheye = new FisheyeGitRepositoryBrowser(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = fisheye.getDiffLink(path); + EditType editType = path.getEditType(); + String slash = repoUrl.endsWith("/") ? "" : "/"; + URL expectedDiffLink = new URL(repoUrl + slash + path.getPath() + "?r1=" + sample.parent + "&r2=" + sample.id); + if (editType == EditType.DELETE || editType == EditType.ADD) { + expectedDiffLink = null; + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + FisheyeGitRepositoryBrowser fisheye = new FisheyeGitRepositoryBrowser(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = fisheye.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = null; + String slash = repoUrl.endsWith("/") ? "" : "/"; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedFileLink = new URL(repoUrl + slash + path.getPath()); + } else if (editType == EditType.DELETE) { + expectedFileLink = new URL(repoUrl + slash + path.getPath()); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitBlitRepositoryBrowserTest.java b/src/test/java/hudson/plugins/git/browser/GitBlitRepositoryBrowserTest.java new file mode 100644 index 0000000000..1a40ceef51 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitBlitRepositoryBrowserTest.java @@ -0,0 +1,83 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.scm.EditType; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitBlitRepositoryBrowserTest { + + private final String repoUrl = "http://gitblit.example.com/"; + + private final boolean useAuthorName; + private final GitChangeSetSample sample; + private final String projectName = "gitBlitProjectName"; + + public GitBlitRepositoryBrowserTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorName() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new GitBlitRepositoryBrowser(repoUrl, projectName)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "commit?r=" + projectName + "&h=" + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + GitBlitRepositoryBrowser gitblit = new GitBlitRepositoryBrowser(repoUrl, projectName); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + EditType editType = path.getEditType(); + assertTrue("Unexpected edit type " + editType.getName(), editType == EditType.ADD || editType == EditType.EDIT || editType == EditType.DELETE); + URL diffLink = gitblit.getDiffLink(path); + URL expectedDiffLink = new URL(repoUrl + "blobdiff?r=" + projectName + "&h=" + sample.id + "&hb=" + sample.parent); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + GitBlitRepositoryBrowser gitblit = new GitBlitRepositoryBrowser(repoUrl, projectName); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = gitblit.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedFileLink = new URL(repoUrl + "blob?r=" + projectName + "&h=" + sample.id + "&f=" + URLEncoder.encode(path.getPath(), "UTF-8").replaceAll("\\+", "%20")); + } else if (editType == EditType.DELETE) { + expectedFileLink = null; + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } + + @Test + public void testGetProjectName() { + GitBlitRepositoryBrowser gitblit = new GitBlitRepositoryBrowser(repoUrl, projectName); + assertEquals(projectName, gitblit.getProjectName()); + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitChangeSetSample.java b/src/test/java/hudson/plugins/git/browser/GitChangeSetSample.java new file mode 100644 index 0000000000..dbf0ae77fa --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitChangeSetSample.java @@ -0,0 +1,40 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import java.util.ArrayList; +import java.util.List; + +public class GitChangeSetSample { + + final GitChangeSet changeSet; + + final String id = "defcc790e89e2f2558d182028cbd4df6602bda2f"; + final String parent = "92ec0aa543f6c871502b0e6f7793a43a4df84519"; + final String authorName = "Mark Waite Author"; + final String committerName = "Mark Waite Committer"; + final String commitTitle = "Revert \"Rename xt.py to my.py, remove xt, add job, modify job\""; + final String addedFileName = "xt"; + final String deletedFileName = "jobs/JENKINS-20585-busy-changelog-prevents-deletion-a.xml"; + final String modifiedFileName = "jobs/git-plugin-my-multi-2.2.x.xml"; + final String renamedFileSrcName = "mt.py"; + final String renamedFileDstName = "xt.py"; + + public GitChangeSetSample(boolean useAuthorName) { + List gitChangeLog = new ArrayList<>(); + gitChangeLog.add("commit " + id); + gitChangeLog.add("tree 9538ba330b18d079bf9792e7cd6362fa7cfc8039"); + gitChangeLog.add("parent " + parent); + gitChangeLog.add("author " + authorName + " 1415842934 -0700"); + gitChangeLog.add("committer " + committerName + " 1415842974 -0700"); + gitChangeLog.add(""); + gitChangeLog.add(" " + commitTitle); + gitChangeLog.add(" "); + gitChangeLog.add(" This reverts commit 92ec0aa543f6c871502b0e6f7793a43a4df84519."); + gitChangeLog.add(""); + gitChangeLog.add(":100644 000000 4378b5b0223f0435eb2365a684e6a544c5c537fc 0000000000000000000000000000000000000000 D\t" + deletedFileName); + gitChangeLog.add(":100644 100644 c305885ca26ad88b0bf96d3bb81e958cf0535194 56aef71694759b71ea76a9dfe377b0e1f8a8388f M\t" + modifiedFileName); + gitChangeLog.add(":000000 120000 0000000000000000000000000000000000000000 fb9953d5d00cb6307954f6d3bf6cb5d2355f62cd A\t" + addedFileName); + gitChangeLog.add(":100755 100755 4099f430ffd37d7e5d60aa08f61daffdccb81b2c 4099f430ffd37d7e5d60aa08f61daffdccb81b2c R100 " + renamedFileSrcName + "\t" + renamedFileDstName); + changeSet = new GitChangeSet(gitChangeLog, useAuthorName); + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitLabTest.java b/src/test/java/hudson/plugins/git/browser/GitLabTest.java new file mode 100644 index 0000000000..5028bd8327 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitLabTest.java @@ -0,0 +1,174 @@ +package hudson.plugins.git.browser; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.GitChangeLogParser; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; +import org.junit.Test; + +public class GitLabTest { + + private static final String GITLAB_URL = "https://SERVER/USER/REPO/"; + private final GitLab gitlab29 = new GitLab(GITLAB_URL, "2.9"); + private final GitLab gitlab42 = new GitLab(GITLAB_URL, "4.2"); + private final GitLab gitlab50 = new GitLab(GITLAB_URL, "5.0"); + private final GitLab gitlab51 = new GitLab(GITLAB_URL, "5.1"); + private final GitLab gitlab711 = new GitLab(GITLAB_URL, "7.11"); /* Which is < 7.2 ! */ +// private final GitLab gitlab7114ee = new GitLab(GITLAB_URL, "7.11.4.ee"); /* Totally borked */ + private final GitLab gitlab7114ee = new GitLab(GITLAB_URL, "7.11"); /* Which is < 7.2 ! */ + private final GitLab gitlab80 = new GitLab(GITLAB_URL, "8.0"); + private final GitLab gitlab87 = new GitLab(GITLAB_URL, "8.7"); + private final GitLab gitlabDefault = new GitLab(GITLAB_URL); + private final GitLab gitlabNaN = new GitLab(GITLAB_URL, "NaN"); + private final GitLab gitlabInfinity = new GitLab(GITLAB_URL, "Infinity"); + private final GitLab gitlabNegative = new GitLab(GITLAB_URL, "-1"); + private final GitLab gitlabGreater = new GitLab(GITLAB_URL, "9999"); + + private final String SHA1 = "396fc230a3db05c427737aa5c2eb7856ba72b05d"; + private final String fileName = "src/main/java/hudson/plugins/git/browser/GithubWeb.java"; + + @Test + public void testGetVersion() { + assertEquals("2.9", gitlab29.getVersion()); + assertEquals("4.2", gitlab42.getVersion()); + assertEquals("5.0", gitlab50.getVersion()); + assertEquals("5.1", gitlab51.getVersion()); + assertEquals("8.7", gitlab87.getVersion()); + assertNull(gitlabDefault.getVersion()); + assertNull(gitlabNaN.getVersion()); + assertNull(gitlabInfinity.getVersion()); + assertEquals("-1.0", gitlabNegative.getVersion()); + assertEquals("9999.0", gitlabGreater.getVersion()); + } + + @Test + public void testGetVersionDouble() { + assertEquals(2.9, gitlab29.getVersionDouble(), .001); + assertEquals(4.2, gitlab42.getVersionDouble(), .001); + assertEquals(5.0, gitlab50.getVersionDouble(), .001); + assertEquals(5.1, gitlab51.getVersionDouble(), .001); + assertEquals(8.7, gitlab87.getVersionDouble(), .001); + assertEquals(Double.POSITIVE_INFINITY, gitlabDefault.getVersionDouble(), .001); + assertEquals(Double.POSITIVE_INFINITY, gitlabNaN.getVersionDouble(), .001); + assertEquals(Double.POSITIVE_INFINITY, gitlabInfinity.getVersionDouble(), .001); + assertEquals(-1.0, gitlabNegative.getVersionDouble(), .001); + assertEquals(9999.0, gitlabGreater.getVersionDouble(), .001); + } + + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { + final GitChangeSet changeSet = createChangeSet("rawchangelog"); + final String expectedURL = GITLAB_URL + "commit/" + SHA1; + assertEquals(expectedURL.replace("commit/", "commits/"), gitlab29.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlab42.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlab50.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlab51.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlab711.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlab7114ee.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlabDefault.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlabNaN.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlabInfinity.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL.replace("commit/", "commits/"), gitlabNegative.getChangeSetLink(changeSet).toString()); + assertEquals(expectedURL, gitlabGreater.getChangeSetLink(changeSet).toString()); + } + + @Test + public void testGetDiffLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path modified1 = pathMap.get(fileName); + final String expectedPre30 = GITLAB_URL + "commits/" + SHA1 + "#" + fileName; + final String expectedPre80 = GITLAB_URL + "commit/" + SHA1 + "#" + fileName; + final String expectedURL = GITLAB_URL + "commit/" + SHA1 + "#" + "diff-0"; + final String expectedDefault = expectedURL; + assertEquals(expectedPre30, gitlabNegative.getDiffLink(modified1).toString()); + assertEquals(expectedPre30, gitlab29.getDiffLink(modified1).toString()); + assertEquals(expectedPre80, gitlab42.getDiffLink(modified1).toString()); + assertEquals(expectedPre80, gitlab50.getDiffLink(modified1).toString()); + assertEquals(expectedPre80, gitlab51.getDiffLink(modified1).toString()); + assertEquals(expectedPre80, gitlab711.getDiffLink(modified1).toString()); + assertEquals(expectedPre80, gitlab7114ee.getDiffLink(modified1).toString()); + assertEquals(expectedURL, gitlab80.getDiffLink(modified1).toString()); + assertEquals(expectedURL, gitlabGreater.getDiffLink(modified1).toString()); + + assertEquals(expectedDefault, gitlabDefault.getDiffLink(modified1).toString()); + assertEquals(expectedDefault, gitlabNaN.getDiffLink(modified1).toString()); + assertEquals(expectedDefault, gitlabInfinity.getDiffLink(modified1).toString()); + } + + @Test + public void testGetFileLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path path = pathMap.get(fileName); + final String expectedURL = GITLAB_URL + "blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/" + fileName; + final String expectedV29 = expectedURL.replace("blob/", "tree/"); + final String expectedV50 = GITLAB_URL + "396fc230a3db05c427737aa5c2eb7856ba72b05d/tree/" + fileName; + assertEquals(expectedV29, gitlabNegative.getFileLink(path).toString()); + assertEquals(expectedV29, gitlab29.getFileLink(path).toString()); + assertEquals(expectedV29, gitlab42.getFileLink(path).toString()); + assertEquals(expectedV50, gitlab50.getFileLink(path).toString()); + assertEquals(expectedURL, gitlab51.getFileLink(path).toString()); + assertEquals(expectedURL, gitlab711.getFileLink(path).toString()); + assertEquals(expectedURL, gitlab7114ee.getFileLink(path).toString()); + assertEquals(expectedURL, gitlabDefault.getFileLink(path).toString()); + assertEquals(expectedURL, gitlabNaN.getFileLink(path).toString()); + assertEquals(expectedURL, gitlabInfinity.getFileLink(path).toString()); + assertEquals(expectedURL, gitlabGreater.getFileLink(path).toString()); + } + + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); + final String fileName = "bar"; + final Path path = pathMap.get(fileName); + final String SHA1 = "fc029da233f161c65eb06d0f1ed4f36ae81d1f4f"; + final String expectedPre30 = GITLAB_URL + "commits/" + SHA1 + "#" + fileName; + final String expectedPre80 = GITLAB_URL + "commit/" + SHA1 + "#" + fileName; + final String expectedURL = GITLAB_URL + "commit/" + SHA1 + "#" + "diff-0"; + final String expectedDefault = expectedURL; + + assertEquals(expectedPre30, gitlabNegative.getFileLink(path).toString()); + assertEquals(expectedPre30, gitlab29.getFileLink(path).toString()); + assertEquals(expectedPre80, gitlab42.getFileLink(path).toString()); + assertEquals(expectedPre80, gitlab50.getFileLink(path).toString()); + assertEquals(expectedPre80, gitlab51.getFileLink(path).toString()); + assertEquals(expectedPre80, gitlab711.getFileLink(path).toString()); + assertEquals(expectedPre80, gitlab7114ee.getFileLink(path).toString()); + assertEquals(expectedURL, gitlab80.getFileLink(path).toString()); + assertEquals(expectedURL, gitlabGreater.getFileLink(path).toString()); + + assertEquals(expectedDefault, gitlabDefault.getFileLink(path).toString()); + assertEquals(expectedDefault, gitlabNaN.getFileLink(path).toString()); + assertEquals(expectedDefault, gitlabInfinity.getFileLink(path).toString()); + + } + + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? "Default" : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(GitLabTest.class.getResourceAsStream(rawchangelogpath)); + return changeSetList.get(0); + } + + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); + final Collection changeSet = createChangeSet(changelog).getPaths(); + for (final Path path : changeSet) { + pathMap.put(path.getPath(), path); + } + return pathMap; + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitLabWorkflowTest.java b/src/test/java/hudson/plugins/git/browser/GitLabWorkflowTest.java new file mode 100644 index 0000000000..bc3f9c2d49 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitLabWorkflowTest.java @@ -0,0 +1,46 @@ +package hudson.plugins.git.browser; + +import jenkins.plugins.git.GitSampleRepoRule; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class GitLabWorkflowTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + @Test + public void checkoutWithVersion() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', browser: [$class: 'GitLab',\n" + + " repoUrl: 'https://a.org/a/b', version: '9.0'],\n" + + " userRemoteConfigs: [[url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void checkoutWithoutVersion() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', browser: [$class: 'GitLab',\n" + + " repoUrl: 'https://a.org/a/b'],\n" + + " userRemoteConfigs: [[url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitListTest.java b/src/test/java/hudson/plugins/git/browser/GitListTest.java new file mode 100644 index 0000000000..18af537757 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitListTest.java @@ -0,0 +1,96 @@ +/** + * Copyright 2010 Mirko Friedenhagen + */ + +package hudson.plugins.git.browser; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.GitChangeLogParser; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * @author mirko + * @author fauxpark + */ +public class GitListTest { + + private static final String GITLIST_URL = "http://gitlist.org/REPO"; + private final GitList gitlist = new GitList(GITLIST_URL); + + @Test + public void testGetUrl() throws IOException { + assertEquals(String.valueOf(gitlist.getUrl()), GITLIST_URL + "/"); + } + + @Test + public void testGetUrlForRepoWithTrailingSlash() throws IOException { + assertEquals(String.valueOf(new GitList(GITLIST_URL + "/").getUrl()), GITLIST_URL + "/"); + } + + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { + final URL changeSetLink = gitlist.getChangeSetLink(createChangeSet("rawchangelog")); + assertEquals(GITLIST_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); + } + + @Test + public void testGetDiffLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path path1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); + assertEquals(GITLIST_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d#1", gitlist.getDiffLink(path1).toString()); + final Path path2 = pathMap.get("src/test/java/hudson/plugins/git/browser/GithubWebTest.java"); + assertEquals(GITLIST_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d#2", gitlist.getDiffLink(path2).toString()); + final Path path3 = pathMap.get("src/test/resources/hudson/plugins/git/browser/rawchangelog-with-deleted-file"); + assertNull("Do not return a diff link for added files.", gitlist.getDiffLink(path3)); + } + + @Test + public void testGetFileLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); + final URL fileLink = gitlist.getFileLink(path); + assertEquals(GITLIST_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); + } + + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); + final Path path = pathMap.get("bar"); + final URL fileLink = gitlist.getFileLink(path); + assertEquals(GITLIST_URL + "/commit/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f#1", String.valueOf(fileLink)); + } + + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(GitListTest.class.getResourceAsStream(rawchangelogpath)); + return changeSetList.get(0); + } + + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); + final Collection changeSet = createChangeSet(changelog).getPaths(); + for (final Path path : changeSet) { + pathMap.put(path.getPath(), path); + } + return pathMap; + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitRepositoryBrowserTest.java b/src/test/java/hudson/plugins/git/browser/GitRepositoryBrowserTest.java new file mode 100644 index 0000000000..336ac7a47f --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitRepositoryBrowserTest.java @@ -0,0 +1,137 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSetUtil; + +import org.eclipse.jgit.lib.ObjectId; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitRepositoryBrowserTest { + + private GitRepositoryBrowser browser; + private GitChangeSet changeSet; + private Collection paths; + + private final String baseURL = "https://github.com/jenkinsci/git-plugin/"; + + private final boolean useAuthorName; + private final String gitImplementation; + private final ObjectId sha1; + + public GitRepositoryBrowserTest(String useAuthorName, String gitImplementation, ObjectId sha1) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + this.gitImplementation = gitImplementation; + this.sha1 = sha1; + } + + @Parameterized.Parameters(name = "{0},{1},{2}") + public static Collection permuteAuthorNameAndGitImplementationAndObjectId() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + String[] implementations = {"git", "jgit"}; + ObjectId[] sha1Array = { // Use commits from git-plugin repo history + ObjectId.fromString("016407404eeda093385ba2ebe9557068b519b669"), // simple commit + ObjectId.fromString("4289aacbb493cfcb78c8276c52e945802942ffd5"), // merge commit + ObjectId.fromString("daf453dfc43db81ede5cde60d0469fda0b3321ab"), // simple commit + ObjectId.fromString("c685e980a502fa10e3a5fa08e02ab4194950c1df"), // Introduced findbugs warning + ObjectId.fromString("8e4ef541b8f319fd2019932a6cddfc480fc7ca28"), // Old commit + ObjectId.fromString("75ef0cde74e01f16b6da075d67cf88b3503067f5"), // First commit - no files, no parent + }; + for (String authorName : allowed) { + for (String gitImplementation : implementations) { + for (ObjectId sha1 : sha1Array) { + Object[] combination = {authorName, gitImplementation, sha1}; + values.add(combination); + } + } + } + return values; + } + + @Before + public void setUp() throws IOException, InterruptedException { + browser = new GitRepositoryBrowserImpl(); + changeSet = GitChangeSetUtil.genChangeSet(sha1, gitImplementation, useAuthorName); + paths = changeSet.getPaths(); + } + + @Test + public void testGetRepoUrl() { + assertThat(browser.getRepoUrl(), is(nullValue())); + } + + @Test + public void testGetDiffLink() throws Exception { + for (GitChangeSet.Path path : paths) { + assertThat(browser.getDiffLink(path), is(getURL(path, true))); + } + } + + @Test + public void testGetFileLink() throws Exception { + for (GitChangeSet.Path path : paths) { + assertThat(browser.getFileLink(path), is(getURL(path, false))); + } + } + + @Test + public void testGetNormalizeUrl() { + assertThat(browser.getNormalizeUrl(), is(true)); + } + + @Test + public void testGetIndexOfPath() throws Exception { + Set foundLocations = new HashSet<>(paths.size()); + for (GitChangeSet.Path path : paths) { + int location = browser.getIndexOfPath(path); + + // Assert that location is in bounds + assertThat(location, is(lessThan(paths.size()))); + assertThat(location, is(greaterThan(-1))); + + // Assert that location has not been seen before + assertThat(foundLocations, not(hasItem(location))); + foundLocations.add(location); + } + + // Assert that exact number of locations were found + assertThat(foundLocations.size(), is(paths.size())); + } + + private URL getURL(GitChangeSet.Path path, boolean isDiffLink) throws MalformedURLException { + return new URL(baseURL + path.getPath() + (isDiffLink ? "-diff-link" : "-file-link")); + } + + public class GitRepositoryBrowserImpl extends GitRepositoryBrowser { + + public URL getDiffLink(GitChangeSet.Path path) throws IOException { + return getURL(path, true); + } + + public URL getFileLink(GitChangeSet.Path path) throws IOException { + return getURL(path, false); + } + + @Override + public URL getChangeSetLink(GitChangeSet e) throws IOException { + throw new UnsupportedOperationException("Not implemented."); + } + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitWebTest.java b/src/test/java/hudson/plugins/git/browser/GitWebTest.java index 109a60da1f..615868a630 100644 --- a/src/test/java/hudson/plugins/git/browser/GitWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/GitWebTest.java @@ -1,105 +1,79 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; -import junit.framework.TestCase; +import static org.junit.Assert.*; +import org.junit.Test; -import org.xml.sax.SAXException; +public class GitWebTest { - -public class GitWebTest extends TestCase { - - /** - * - */ private static final String GITWEB_URL = "https://SERVER/gitweb?repo.git"; private final GitWeb gitwebWeb = new GitWeb(GITWEB_URL); - - /** - * Test method for {@link hudson.plugins.git.browser.GitWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(gitwebWeb.getUrl()), GITWEB_URL); } - /** - * Test method for {@link hudson.plugins.git.browser.GitWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = gitwebWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(GITWEB_URL + "&a=commit&h=396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GitWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path modified1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(GITWEB_URL + "&a=blobdiff&f=src/main/java/hudson/plugins/git/browser/GithubWeb.java&fp=src/main/java/hudson/plugins/git/browser/GithubWeb.java&h=3f28ad75f5ecd5e0ea9659362e2eef18951bd451&hp=2e0756cd853dccac638486d6aab0e74bc2ef4041&hb=396fc230a3db05c427737aa5c2eb7856ba72b05d&hpb=f28f125f4cc3e5f6a32daee6a26f36f7b788b8ff", gitwebWeb.getDiffLink(modified1).toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { - final HashMap pathMap = createPathMap("rawchangelog"); + @Test + public void testGetFileLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = gitwebWeb.getFileLink(path); assertEquals(GITWEB_URL + "&a=blob&f=src/main/java/hudson/plugins/git/browser/GithubWeb.java&h=2e0756cd853dccac638486d6aab0e74bc2ef4041&hb=396fc230a3db05c427737aa5c2eb7856ba72b05d", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = gitwebWeb.getFileLink(path); assertEquals(GITWEB_URL + "&a=blob&f=bar&h=257cc5642cb1a054f08cc83f2d943e56fd3ebe99&hb=fc029da233f161c65eb06d0f1ed4f36ae81d1f4f", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(GitWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, random.nextBoolean()); + final List changeSetList = logParser.parse(GitWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/GithubWebTest.java b/src/test/java/hudson/plugins/git/browser/GithubWebTest.java index de36e8638d..8c8a21e721 100644 --- a/src/test/java/hudson/plugins/git/browser/GithubWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/GithubWebTest.java @@ -4,66 +4,58 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import hudson.plugins.git.GitSCM; +import hudson.scm.RepositoryBrowser; +import jenkins.plugins.git.AbstractGitSCMSource; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; +import jenkins.scm.api.SCMHead; +import org.eclipse.jgit.transport.RefSpec; -import junit.framework.TestCase; - -import org.xml.sax.SAXException; +import static org.junit.Assert.*; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; /** * @author mirko - * */ -public class GithubWebTest extends TestCase { +public class GithubWebTest { - /** - * - */ private static final String GITHUB_URL = "http://github.com/USER/REPO"; private final GithubWeb githubWeb = new GithubWeb(GITHUB_URL); - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(githubWeb.getUrl()), GITHUB_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new GithubWeb(GITHUB_URL + "/").getUrl()), GITHUB_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = githubWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(GITHUB_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(GITHUB_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d#diff-0", githubWeb.getDiffLink(path1).toString()); @@ -73,45 +65,154 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertNull("Do not return a diff link for added files.", githubWeb.getDiffLink(path3)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = githubWeb.getFileLink(path); assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Issue("JENKINS-42597") + @Test + public void testGetFileLinkPathWithEscape() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-escape"); + final Path path = pathMap.get("src/test/java/hudson/plugins/git/browser/conf%.txt"); + final URL fileLink = githubWeb.getFileLink(path); + assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/test/java/hudson/plugins/git/browser/conf%25.txt", String.valueOf(fileLink)); + } + @Issue("JENKINS-42597") + @Test + public void testGetFileLinkPathWithWindowsUnescapeChar() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-escape"); + final Path path = pathMap.get("src/test/java/hudson/plugins/git/browser/conf^%.txt"); + final URL fileLink = githubWeb.getFileLink(path); + assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/test/java/hudson/plugins/git/browser/conf%5E%25.txt", String.valueOf(fileLink)); + } + + @Issue("JENKINS-42597") + @Test + public void testGetFileLinkPathWithDoubleEscape() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-escape"); + final Path path = pathMap.get("src/test/java/hudson/plugins/git/browser/conf%%.txt"); + final URL fileLink = githubWeb.getFileLink(path); + assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/test/java/hudson/plugins/git/browser/conf%25%25.txt", String.valueOf(fileLink)); + } + + @Issue("JENKINS-42597") + @Test + public void testGetFileLinkPathWithWindowsEnvironmentalVariable() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-escape"); + final Path path = pathMap.get("src/test/java/hudson/plugins/git/browser/conf%abc%.txt"); + final URL fileLink = githubWeb.getFileLink(path); + assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/test/java/hudson/plugins/git/browser/conf%25abc%25.txt", String.valueOf(fileLink)); + } + + @Issue("JENKINS-42597") + @Test + public void testGetFileLinkPathWithSpaceInName() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-escape"); + final Path path = pathMap.get("src/test/java/hudson/plugins/git/browser/config file.txt"); + final URL fileLink = githubWeb.getFileLink(path); + assertEquals(GITHUB_URL + "/blob/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/test/java/hudson/plugins/git/browser/config%20file.txt", String.valueOf(fileLink)); + } + + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = githubWeb.getFileLink(path); assertEquals(GITHUB_URL + "/commit/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f#diff-0", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(GithubWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private String repoUrl(String baseUrl, boolean add_git_suffix, boolean add_slash_suffix) { + return baseUrl + (add_git_suffix ? ".git" : "") + (add_slash_suffix ? "/" : ""); + } + + @Test + public void testGuessBrowser() { + assertGuessURL("https://github.com/kohsuke/msv.git", "https://github.com/kohsuke/msv/"); + assertGuessURL("https://github.com/kohsuke/msv/", "https://github.com/kohsuke/msv/"); + assertGuessURL("https://github.com/kohsuke/msv", "https://github.com/kohsuke/msv/"); + assertGuessURL("git@github.com:kohsuke/msv.git", "https://github.com/kohsuke/msv/"); + assertGuessURL("git@git.apache.org:whatever.git", null); + final boolean allowed [] = { Boolean.TRUE, Boolean.FALSE }; + for (final boolean add_git_suffix : allowed) { + for (final boolean add_slash_suffix : allowed) { + assertGuessURL(repoUrl("git@github.com:kohsuke/msv", add_git_suffix, add_slash_suffix), "https://github.com/kohsuke/msv/"); + assertGuessURL(repoUrl("https://github.com/kohsuke/msv", add_git_suffix, add_slash_suffix), "https://github.com/kohsuke/msv/"); + assertGuessURL(repoUrl("ssh://github.com/kohsuke/msv", add_git_suffix, add_slash_suffix), "https://github.com/kohsuke/msv/"); + assertGuessURL(repoUrl("ssh://git@github.com/kohsuke/msv", add_git_suffix, add_slash_suffix), "https://github.com/kohsuke/msv/"); + } + } + } + + private void assertGuessURL(String repo, String web) { + RepositoryBrowser guess = new GitSCM(repo).guessBrowser(); + String actual = guess instanceof GithubWeb ? ((GithubWeb) guess).getRepoUrl() : null; + assertEquals("For repo '" + repo + "':", web, actual); + } + + @Issue("JENKINS-33409") + @Test + public void guessBrowserSCMSource() throws Exception { + // like GitSCMSource: + assertGuessURL("https://github.com/kohsuke/msv.git", "https://github.com/kohsuke/msv/", "+refs/heads/*:refs/remotes/origin/*"); + // like GitHubSCMSource: + assertGuessURL("https://github.com/kohsuke/msv.git", "https://github.com/kohsuke/msv/", "+refs/heads/*:refs/remotes/origin/*", "+refs/pull/*/merge:refs/remotes/origin/pr/*"); + } + + private void assertGuessURL(String remote, String web, String... refSpecs) { + RepositoryBrowser guess = new MockSCMSource(remote, refSpecs).build(new SCMHead("master")).guessBrowser(); + String actual = guess instanceof GithubWeb ? ((GithubWeb) guess).getRepoUrl() : null; + assertEquals(web, actual); + } + + private static class MockSCMSource extends AbstractGitSCMSource { + private final String remote; + private final String[] refSpecs; + MockSCMSource(String remote, String[] refSpecs) { + this.remote = remote; + this.refSpecs = refSpecs; + } + @Override + public String getCredentialsId() { + return null; + } + @Override + public String getRemote() { + return remote; + } + @Override + public String getIncludes() { + return "*"; + } + @Override + public String getExcludes() { + return ""; + } + @Override + protected List getRefSpecs() { + List result = new ArrayList<>(); + for (String refSpec : refSpecs) { + result.add(new RefSpec(refSpec)); + } + return result; + } + } + + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(GithubWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); @@ -119,5 +220,4 @@ private HashMap createPathMap(final String changelog) throws IOExc return pathMap; } - } diff --git a/src/test/java/hudson/plugins/git/browser/GitilesTest.java b/src/test/java/hudson/plugins/git/browser/GitilesTest.java new file mode 100644 index 0000000000..ba7018164b --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GitilesTest.java @@ -0,0 +1,64 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GitilesTest { + + private final String repoUrl = "https://gwt.googlesource.com/gwt/"; + + private final boolean useAuthorName; + private final GitChangeSetSample sample; + + public GitilesTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorName() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new Gitiles(repoUrl)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "+/" + sample.id + "%5E%21"), result); + } + + @Test + public void testGetDiffLink() throws Exception { + Gitiles gitiles = new Gitiles(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = gitiles.getDiffLink(path); + URL expectedDiffLink = new URL(repoUrl + "+/" + sample.id + "%5E%21"); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + path.getEditType().getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + Gitiles gitiles = new Gitiles(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = gitiles.getFileLink(path); + URL expectedFileLink = new URL(repoUrl + "+blame/" + sample.id + "/" + path.getPath()); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + path.getEditType().getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } +} diff --git a/src/test/java/hudson/plugins/git/browser/GitoriousWebTest.java b/src/test/java/hudson/plugins/git/browser/GitoriousWebTest.java index 9d7645f943..09b44e6fab 100644 --- a/src/test/java/hudson/plugins/git/browser/GitoriousWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/GitoriousWebTest.java @@ -1,63 +1,47 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; -import junit.framework.TestCase; +import static org.junit.Assert.*; +import org.junit.Test; -import org.xml.sax.SAXException; +public class GitoriousWebTest { - -public class GitoriousWebTest extends TestCase { - - /** - * - */ private static final String GITORIOUS_URL = "https://SERVER/PROJECT"; private final GitoriousWeb gitoriousWeb = new GitoriousWeb(GITORIOUS_URL); - - /** - * Test method for {@link hudson.plugins.git.browser.GitoriousWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(gitoriousWeb.getUrl()), GITORIOUS_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.GitoriousWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new GitoriousWeb(GITORIOUS_URL + "/").getUrl()), GITORIOUS_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.GitoriousWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = gitoriousWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(GITORIOUS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GitoriousWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path modified1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(GITORIOUS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d/diffs?diffmode=sidebyside&fragment=1#src/main/java/hudson/plugins/git/browser/GithubWeb.java", gitoriousWeb.getDiffLink(modified1).toString()); @@ -66,51 +50,38 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertEquals(GITORIOUS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d/diffs?diffmode=sidebyside&fragment=1#src/test/resources/hudson/plugins/git/browser/rawchangelog-with-deleted-file", gitoriousWeb.getDiffLink(added).toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = gitoriousWeb.getFileLink(path); assertEquals(GITORIOUS_URL + "/blobs/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = gitoriousWeb.getFileLink(path); assertEquals(GITORIOUS_URL + "/commit/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f/diffs?diffmode=sidebyside&fragment=1#bar", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(GitoriousWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(GitoriousWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/GogsGitTest.java b/src/test/java/hudson/plugins/git/browser/GogsGitTest.java new file mode 100644 index 0000000000..81d1806eca --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/GogsGitTest.java @@ -0,0 +1,91 @@ +package hudson.plugins.git.browser; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.GitChangeLogParser; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * @author Norbert Lange (nolange79@gmail.com) + */ +public class GogsGitTest { + + private static final String GOGS_URL = "http://USER.kilnhg.com/Code/PROJECT/Group/REPO"; + private final GogsGit GogsGit = new GogsGit(GOGS_URL); + + @Test + public void testGetUrl() throws IOException { + assertEquals(String.valueOf(GogsGit.getUrl()), GOGS_URL + "/"); + } + + @Test + public void testGetUrlForRepoWithTrailingSlash() throws IOException { + assertEquals(String.valueOf(new GogsGit(GOGS_URL + "/").getUrl()), GOGS_URL + "/"); + } + + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { + final URL changeSetLink = GogsGit.getChangeSetLink(createChangeSet("rawchangelog")); + assertEquals(GOGS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); + } + + @Test + public void testGetDiffLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path path1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); + assertEquals(GOGS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d#diff-1", GogsGit.getDiffLink(path1).toString()); + final Path path2 = pathMap.get("src/test/java/hudson/plugins/git/browser/GithubWebTest.java"); + assertEquals(GOGS_URL + "/commit/396fc230a3db05c427737aa5c2eb7856ba72b05d#diff-2", GogsGit.getDiffLink(path2).toString()); + final Path path3 = pathMap.get("src/test/resources/hudson/plugins/git/browser/rawchangelog-with-deleted-file"); + assertNull("Do not return a diff link for added files.", GogsGit.getDiffLink(path3)); + } + + @Test + public void testGetFileLinkPath() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog"); + final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); + final URL fileLink = GogsGit.getFileLink(path); + assertEquals(GOGS_URL + "/src/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); + } + + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { + final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); + final Path path = pathMap.get("bar"); + final URL fileLink = GogsGit.getFileLink(path); + assertEquals(GOGS_URL + "/commit/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f#diff-1", String.valueOf(fileLink)); + } + + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(GogsGitTest.class.getResourceAsStream(rawchangelogpath)); + return changeSetList.get(0); + } + + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); + final Collection changeSet = createChangeSet(changelog).getPaths(); + for (final Path path : changeSet) { + pathMap.put(path.getPath(), path); + } + return pathMap; + } +} diff --git a/src/test/java/hudson/plugins/git/browser/KilnGitTest.java b/src/test/java/hudson/plugins/git/browser/KilnGitTest.java index 3ac35529ef..61f75c1a5b 100644 --- a/src/test/java/hudson/plugins/git/browser/KilnGitTest.java +++ b/src/test/java/hudson/plugins/git/browser/KilnGitTest.java @@ -1,62 +1,50 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; -import junit.framework.TestCase; - -import org.xml.sax.SAXException; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author Chris Klaiber (cklaiber@gmail.com) */ -public class KilnGitTest extends TestCase { +public class KilnGitTest { private static final String KILN_URL = "http://USER.kilnhg.com/Code/PROJECT/Group/REPO"; private final KilnGit kilnGit = new KilnGit(KILN_URL); - - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(kilnGit.getUrl()), KILN_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new KilnGit(KILN_URL + "/").getUrl()), KILN_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = kilnGit.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(KILN_URL + "/History/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(KILN_URL + "/History/396fc230a3db05c427737aa5c2eb7856ba72b05d#diff-1", kilnGit.getDiffLink(path1).toString()); @@ -66,51 +54,38 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertNull("Do not return a diff link for added files.", kilnGit.getDiffLink(path3)); } - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = kilnGit.getFileLink(path); assertEquals(KILN_URL + "/FileHistory/src/main/java/hudson/plugins/git/browser/GithubWeb.java?rev=396fc230a3db05c427737aa5c2eb7856ba72b05d", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.KilnGit#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = kilnGit.getFileLink(path); assertEquals(KILN_URL + "/History/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f#diff-1", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(KilnGitTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(KilnGitTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/PhabricatorTest.java b/src/test/java/hudson/plugins/git/browser/PhabricatorTest.java new file mode 100644 index 0000000000..904404e43d --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/PhabricatorTest.java @@ -0,0 +1,53 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import java.io.IOException; +import java.net.URL; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PhabricatorTest { + + private final String repoName = "phabricatorRepo"; + private final String repoUrl = "http://phabricator.example.com/"; + private final Phabricator phabricator; + + private final GitChangeSetSample sample; + + public PhabricatorTest() { + phabricator = new Phabricator(repoUrl, repoName); + sample = new GitChangeSetSample(true); + } + + @Test + public void testGetRepo() throws IOException { + assertEquals(repoName, phabricator.getRepo()); + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = phabricator.getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "r" + repoName + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = phabricator.getDiffLink(path); + URL expectedDiffLink = new URL(repoUrl + "diffusion/" + repoName + "/change/master/" + path.getPath() + ";" + sample.id); + String msg = "Wrong link for path: " + path.getPath(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = phabricator.getDiffLink(path); + URL expectedFileLink = new URL(repoUrl + "diffusion/" + repoName + "/change/master/" + path.getPath() + ";" + sample.id); + String msg = "Wrong link for path: " + path.getPath(); + assertEquals(msg, expectedFileLink, fileLink); + } + } + +} diff --git a/src/test/java/hudson/plugins/git/browser/RedmineWebTest.java b/src/test/java/hudson/plugins/git/browser/RedmineWebTest.java index a2383fa9c0..af0fb384de 100644 --- a/src/test/java/hudson/plugins/git/browser/RedmineWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/RedmineWebTest.java @@ -1,64 +1,49 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; - -import junit.framework.TestCase; - -import org.xml.sax.SAXException; +import java.util.Random; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author mfriedenhagen */ -public class RedmineWebTest extends TestCase { +public class RedmineWebTest { - /** - * - */ private static final String REDMINE_URL = "https://SERVER/PATH/projects/PROJECT/repository"; private final RedmineWeb redmineWeb = new RedmineWeb(REDMINE_URL); - /** - * Test method for {@link hudson.plugins.git.browser.RedmineWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(redmineWeb.getUrl()), REDMINE_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.RedmineWeb#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new RedmineWeb(REDMINE_URL + "/").getUrl()), REDMINE_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.RedmineWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = redmineWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(REDMINE_URL + "/diff?rev=396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.RedmineWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path modified1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(REDMINE_URL + "/revisions/396fc230a3db05c427737aa5c2eb7856ba72b05d/diff/src/main/java/hudson/plugins/git/browser/GithubWeb.java", redmineWeb.getDiffLink(modified1).toString()); @@ -69,51 +54,38 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertEquals(REDMINE_URL + "/revisions/396fc230a3db05c427737aa5c2eb7856ba72b05d/entry/src/test/resources/hudson/plugins/git/browser/rawchangelog-with-deleted-file", redmineWeb.getDiffLink(added).toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = redmineWeb.getFileLink(path); assertEquals(REDMINE_URL + "/revisions/396fc230a3db05c427737aa5c2eb7856ba72b05d/entry/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = redmineWeb.getFileLink(path); assertEquals(REDMINE_URL + "/revisions/fc029da233f161c65eb06d0f1ed4f36ae81d1f4f/diff/bar", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(RedmineWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(RedmineWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/RhodeCodeTest.java b/src/test/java/hudson/plugins/git/browser/RhodeCodeTest.java index 9d897d7f52..f080d4e991 100644 --- a/src/test/java/hudson/plugins/git/browser/RhodeCodeTest.java +++ b/src/test/java/hudson/plugins/git/browser/RhodeCodeTest.java @@ -1,115 +1,87 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; -import junit.framework.TestCase; +import static org.junit.Assert.*; +import org.junit.Test; -import org.xml.sax.SAXException; +public class RhodeCodeTest { - -public class RhodeCodeTest extends TestCase { - - /** - * - */ private static final String RHODECODE_URL = "https://SERVER/r/PROJECT"; private final RhodeCode rhodecode = new RhodeCode(RHODECODE_URL); - /** - * Test method for {@link hudson.plugins.git.browser.RhodeCode#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(rhodecode.getUrl()), RHODECODE_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.RhodeCode#getUrl()}. - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new RhodeCode(RHODECODE_URL + "/").getUrl()), RHODECODE_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.RhodeCode#getChangeSetLink(hudson.plugins.git.GitChangeSet)}. - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = rhodecode.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals(RHODECODE_URL + "/changeset/396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.RhodeCode#getDiffLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path modified1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); - assertEquals(RHODECODE_URL + "/diff/src/main/java/hudson/plugins/git/browser/GithubWeb.java?diff2=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff1=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff=diff+to+revision", rhodecode.getDiffLink(modified1).toString()); + assertEquals(RHODECODE_URL + "/diff/src/main/java/hudson/plugins/git/browser/GithubWeb.java?diff2=f28f125f4cc3e5f6a32daee6a26f36f7b788b8ff&diff1=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff=diff+to+revision", rhodecode.getDiffLink(modified1).toString()); // For added files returns a link to the commit. final Path added = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); - assertEquals(RHODECODE_URL + "/diff/src/main/java/hudson/plugins/git/browser/GithubWeb.java?diff2=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff1=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff=diff+to+revision", rhodecode.getDiffLink(added).toString()); + assertEquals(RHODECODE_URL + "/diff/src/main/java/hudson/plugins/git/browser/GithubWeb.java?diff2=f28f125f4cc3e5f6a32daee6a26f36f7b788b8ff&diff1=396fc230a3db05c427737aa5c2eb7856ba72b05d&diff=diff+to+revision", rhodecode.getDiffLink(added).toString()); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = rhodecode.getFileLink(path); assertEquals(RHODECODE_URL + "/files/396fc230a3db05c427737aa5c2eb7856ba72b05d/src/main/java/hudson/plugins/git/browser/GithubWeb.java", String.valueOf(fileLink)); } - /** - * Test method for {@link hudson.plugins.git.browser.GithubWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)}. - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = rhodecode.getFileLink(path); assertEquals(RHODECODE_URL + "/files/b547aa10c3f06710c6fdfcdb2a9149c81662923b/bar", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(RhodeCodeTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(RhodeCodeTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - - } diff --git a/src/test/java/hudson/plugins/git/browser/StashTest.java b/src/test/java/hudson/plugins/git/browser/StashTest.java new file mode 100644 index 0000000000..b6398c3f85 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/StashTest.java @@ -0,0 +1,81 @@ +package hudson.plugins.git.browser; + +import hudson.plugins.git.GitChangeSet; +import hudson.scm.EditType; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class StashTest { + + private final String repoUrl = "http://stash.example.com/"; + + private final boolean useAuthorName; + private final GitChangeSetSample sample; + + public StashTest(String useAuthorName) { + this.useAuthorName = Boolean.valueOf(useAuthorName); + sample = new GitChangeSetSample(this.useAuthorName); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection permuteAuthorName() { + List values = new ArrayList<>(); + String[] allowed = {"true", "false"}; + for (String authorName : allowed) { + Object[] combination = {authorName}; + values.add(combination); + } + return values; + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = (new Stash(repoUrl)).getChangeSetLink(sample.changeSet); + assertEquals(new URL(repoUrl + "commits/" + sample.id), result); + } + + @Test + public void testGetDiffLink() throws Exception { + Stash stash = new Stash(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = stash.getDiffLink(path); + EditType editType = path.getEditType(); + URL expectedDiffLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedDiffLink = new URL(repoUrl + "diff/" + path.getPath() + "?at=" + sample.id + "&until=" + sample.id); + } else if (editType == EditType.DELETE) { + expectedDiffLink = new URL(repoUrl + "diff/" + path.getPath() + "?at=" + sample.parent + "&until=" + sample.id); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + Stash stash = new Stash(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = stash.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = null; + if (editType == EditType.ADD || editType == EditType.EDIT) { + expectedFileLink = new URL(repoUrl + "browse/" + path.getPath() + "?at=" + sample.id); + } else if (editType == EditType.DELETE) { + expectedFileLink = new URL(repoUrl + "browse/" + path.getPath() + "?at=" + sample.parent); + } else { + fail("Unexpected edit type " + editType.getName()); + } + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } +} diff --git a/src/test/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowserTest.java b/src/test/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowserTest.java new file mode 100644 index 0000000000..fa1f0a816d --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/TFS2013GitRepositoryBrowserTest.java @@ -0,0 +1,95 @@ +package hudson.plugins.git.browser; + +import hudson.model.*; +import hudson.plugins.git.*; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.scm.ChangeLogSet; +import hudson.scm.EditType; +import org.jenkinsci.plugins.gitclient.JGitTool; +import org.junit.Test; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TFS2013GitRepositoryBrowserTest { + + private static final String projectName = "fisheyeProjectName"; + + private final String repoUrl; + private final GitChangeSetSample sample; + + public TFS2013GitRepositoryBrowserTest() { + this.repoUrl = "http://tfs/tfs/project/_git/repo"; + sample = new GitChangeSetSample(false); + + GitSCM scm = new GitSCM( + Collections.singletonList(new UserRemoteConfig(repoUrl, null, null, null)), + new ArrayList<>(), + false, Collections.emptyList(), + null, JGitTool.MAGIC_EXENAME, + Collections.emptyList()); + + AbstractProject project = mock(AbstractProject.class); + AbstractBuild build = mock(AbstractBuild.class); + + when(project.getScm()).thenReturn(scm); + when(build.getProject()).thenReturn(project); + + sample.changeSet.setParent(ChangeLogSet.createEmpty((Run) build)); + } + + @Test + public void testResolveURLFromSCM() throws Exception { + TFS2013GitRepositoryBrowser browser = new TFS2013GitRepositoryBrowser(""); + assertThat(browser.getRepoUrl(sample.changeSet).toString(), is("http://tfs/tfs/project/_git/repo/")); + } + + @Test + public void testResolveURLFromConfig() throws Exception { + TFS2013GitRepositoryBrowser browser = new TFS2013GitRepositoryBrowser("http://url/repo"); + assertThat(browser.getRepoUrl(sample.changeSet).toString(), is("http://url/repo/")); + } + + @Test + public void testResolveURLFromConfigWithTrailingSlash() throws Exception { + TFS2013GitRepositoryBrowser browser = new TFS2013GitRepositoryBrowser("http://url/repo/"); + assertThat(browser.getRepoUrl(sample.changeSet).toString(), is("http://url/repo/")); + } + + @Test + public void testGetChangeSetLink() throws Exception { + URL result = new TFS2013GitRepositoryBrowser(repoUrl).getChangeSetLink(sample.changeSet); + assertThat(result.toString(), is("http://tfs/tfs/project/_git/repo/commit/" + sample.id)); + } + + @Test + public void testGetDiffLink() throws Exception { + TFS2013GitRepositoryBrowser browser = new TFS2013GitRepositoryBrowser(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL diffLink = browser.getDiffLink(path); + EditType editType = path.getEditType(); + URL expectedDiffLink = new URL("http://tfs/tfs/project/_git/repo/commit/" + sample.id + "#path=" + path.getPath() + "&_a=compare"); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedDiffLink, diffLink); + } + } + + @Test + public void testGetFileLink() throws Exception { + TFS2013GitRepositoryBrowser browser = new TFS2013GitRepositoryBrowser(repoUrl); + for (GitChangeSet.Path path : sample.changeSet.getPaths()) { + URL fileLink = browser.getFileLink(path); + EditType editType = path.getEditType(); + URL expectedFileLink = new URL("http://tfs/tfs/project/_git/repo/commit/" + sample.id + "#path=" + path.getPath() + "&_a=history"); + String msg = "Wrong link for path: " + path.getPath() + ", edit type: " + editType.getName(); + assertEquals(msg, expectedFileLink, fileLink); + } + } +} diff --git a/src/test/java/hudson/plugins/git/browser/ViewGitWebTest.java b/src/test/java/hudson/plugins/git/browser/ViewGitWebTest.java index 28fe89c4ce..bdd2ac3335 100644 --- a/src/test/java/hudson/plugins/git/browser/ViewGitWebTest.java +++ b/src/test/java/hudson/plugins/git/browser/ViewGitWebTest.java @@ -1,70 +1,51 @@ package hudson.plugins.git.browser; +import hudson.EnvVars; +import hudson.model.TaskListener; import hudson.plugins.git.GitChangeLogParser; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitChangeSet.Path; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Random; -import junit.framework.TestCase; - -import org.xml.sax.SAXException; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author Paul Nyheim (paul.nyheim@gmail.com) */ -public class ViewGitWebTest extends TestCase { +public class ViewGitWebTest { private static final String VIEWGIT_URL = "http://SERVER/viewgit"; private static final String PROJECT_NAME = "PROJECT"; private final ViewGitWeb viewGitWeb = new ViewGitWeb(VIEWGIT_URL, PROJECT_NAME); - /** - * Test method for {@link hudson.plugins.git.browser.ViewGitWeb#getUrl()}. - * - * @throws MalformedURLException - */ + @Test public void testGetUrl() throws IOException { assertEquals(String.valueOf(viewGitWeb.getUrl()), VIEWGIT_URL + "/"); } - /** - * Test method for {@link hudson.plugins.git.browser.ViewGitWeb#getUrl()}. - * - * @throws MalformedURLException - */ + @Test public void testGetUrlForRepoWithTrailingSlash() throws IOException { assertEquals(String.valueOf(new ViewGitWeb(VIEWGIT_URL + "/", PROJECT_NAME).getUrl()), VIEWGIT_URL + "/"); } - /** - * Test method for - * {@link hudson.plugins.git.browser.ViewGitWeb#getChangeSetLink(hudson.plugins.git.GitChangeSet)} - * . - * - * @throws SAXException - * @throws IOException - */ - public void testGetChangeSetLinkGitChangeSet() throws IOException, SAXException { + @Test + public void testGetChangeSetLinkGitChangeSet() throws Exception { final URL changeSetLink = viewGitWeb.getChangeSetLink(createChangeSet("rawchangelog")); assertEquals("http://SERVER/viewgit/?p=PROJECT&a=commit&h=396fc230a3db05c427737aa5c2eb7856ba72b05d", changeSetLink.toString()); } - /** - * Test method for - * {@link hudson.plugins.git.browser.ViewGitWeb#getDiffLink(hudson.plugins.git.GitChangeSet.Path)} - * . - * - * @throws SAXException - * @throws IOException - */ - public void testGetDiffLinkPath() throws IOException, SAXException { + @Test + public void testGetDiffLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path1 = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); assertEquals(VIEWGIT_URL + "/?p=PROJECT&a=commitdiff&h=396fc230a3db05c427737aa5c2eb7856ba72b05d#src%2Fmain%2Fjava%2Fhudson%2Fplugins%2Fgit%2Fbrowser%2FGithubWeb.java", viewGitWeb.getDiffLink(path1).toString()); @@ -74,15 +55,8 @@ public void testGetDiffLinkPath() throws IOException, SAXException { assertNull("Do not return a diff link for added files.", viewGitWeb.getDiffLink(path3)); } - /** - * Test method for - * {@link hudson.plugins.git.browser.ViewGitWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)} - * . - * - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPath() throws IOException, SAXException { + @Test + public void testGetFileLinkPath() throws Exception { final HashMap pathMap = createPathMap("rawchangelog"); final Path path = pathMap.get("src/main/java/hudson/plugins/git/browser/GithubWeb.java"); final URL fileLink = viewGitWeb.getFileLink(path); @@ -90,48 +64,38 @@ public void testGetFileLinkPath() throws IOException, SAXException { String.valueOf(fileLink)); } - public void testGetDiffLinkForDeletedFile() throws Exception{ + @Test + public void testGetDiffLinkForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); assertNull("Do not return a diff link for deleted files.", viewGitWeb.getDiffLink(path)); } - /** - * Test method for - * {@link hudson.plugins.git.browser.ViewGitWeb#getFileLink(hudson.plugins.git.GitChangeSet.Path)} - * . - * - * @throws SAXException - * @throws IOException - */ - public void testGetFileLinkPathForDeletedFile() throws IOException, SAXException { + @Test + public void testGetFileLinkPathForDeletedFile() throws Exception { final HashMap pathMap = createPathMap("rawchangelog-with-deleted-file"); final Path path = pathMap.get("bar"); final URL fileLink = viewGitWeb.getFileLink(path); assertEquals(VIEWGIT_URL + "/?p=PROJECT&a=commitdiff&h=fc029da233f161c65eb06d0f1ed4f36ae81d1f4f#bar", String.valueOf(fileLink)); } - private GitChangeSet createChangeSet(String rawchangelogpath) throws IOException, SAXException { - final File rawchangelog = new File(ViewGitWebTest.class.getResource(rawchangelogpath).getFile()); - final GitChangeLogParser logParser = new GitChangeLogParser(false); - final List changeSetList = logParser.parse(null, rawchangelog).getLogs(); + private final Random random = new Random(); + + private GitChangeSet createChangeSet(String rawchangelogpath) throws Exception { + /* Use randomly selected git client implementation since the client implementation should not change result */ + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(new File(".")).using(random.nextBoolean() ? null : "jgit").getClient(); + final GitChangeLogParser logParser = new GitChangeLogParser(gitClient, false); + final List changeSetList = logParser.parse(ViewGitWebTest.class.getResourceAsStream(rawchangelogpath)); return changeSetList.get(0); } - /** - * @param changelog - * @return - * @throws IOException - * @throws SAXException - */ - private HashMap createPathMap(final String changelog) throws IOException, SAXException { - final HashMap pathMap = new HashMap(); + private HashMap createPathMap(final String changelog) throws Exception { + final HashMap pathMap = new HashMap<>(); final Collection changeSet = createChangeSet(changelog).getPaths(); for (final Path path : changeSet) { pathMap.put(path.getPath(), path); } return pathMap; } - } diff --git a/src/test/java/hudson/plugins/git/browser/casc/GitLabConfiguratorTest.java b/src/test/java/hudson/plugins/git/browser/casc/GitLabConfiguratorTest.java new file mode 100644 index 0000000000..47f0085480 --- /dev/null +++ b/src/test/java/hudson/plugins/git/browser/casc/GitLabConfiguratorTest.java @@ -0,0 +1,147 @@ +package hudson.plugins.git.browser.casc; + +import hudson.plugins.git.browser.GitLab; +import io.jenkins.plugins.casc.ConfigurationContext; +import io.jenkins.plugins.casc.model.Mapping; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class GitLabConfiguratorTest { + + private final GitLabConfigurator configurator = new GitLabConfigurator(); + private static final ConfigurationContext NULL_CONFIGURATION_CONTEXT = null; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testGetName() { + assertEquals("gitLab", configurator.getName()); + } + + @Test + public void testGetTarget() { + assertEquals("Wrong target class", configurator.getTarget(), GitLab.class); + } + + @Test + public void testCanConfigure() { + assertTrue("Can't configure AdvisorGlobalConfiguration", configurator.canConfigure(GitLab.class)); + assertFalse("Can configure AdvisorRootConfigurator", configurator.canConfigure(GitLabConfigurator.class)); + } + + @Test + public void testGetImplementedAPI() { + assertEquals("Wrong implemented API", configurator.getImplementedAPI(), GitLab.class); + } + + @Test + public void testGetConfigurators() { + assertThat(configurator.getConfigurators(NULL_CONFIGURATION_CONTEXT), contains(configurator)); + } + + @Test + public void testDescribe() throws Exception { + final Mapping expectedMapping = new Mapping(); + expectedMapping.put("repoUrl", "http://fake"); + expectedMapping.put("version", "1.1"); + final GitLab configuration = new GitLab("http://fake", "1.1"); + + final Mapping described = configurator.describe(configuration, NULL_CONFIGURATION_CONTEXT).asMapping(); + assertEquals(expectedMapping.getScalarValue("repoUrl"), described.getScalarValue("repoUrl")); + assertEquals(expectedMapping.getScalarValue("version"), described.getScalarValue("version")); + } + + @Test + public void testInstance() throws Exception { + final GitLab expectedConfiguration = new GitLab("http://fake", "2.0"); + final Mapping mapping = new Mapping(); + mapping.put("repoUrl", "http://fake"); + mapping.put("version", "2.0"); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + + @Test + public void testInstanceWithEmptyRepo() throws Exception { + final GitLab expectedConfiguration = new GitLab("", "2.0"); + final Mapping mapping = new Mapping(); + mapping.put("repoUrl", ""); + mapping.put("version", "2.0"); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + + } + + @Test + public void testInstanceWithNullRepo() throws Exception { + final GitLab expectedConfiguration = new GitLab(null, "2.0"); + final Mapping mapping = new Mapping(); + mapping.put("version", "2.0"); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertThat(instance.getRepoUrl(), isEmptyString()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + + + @Test + public void testInstanceWithEmptyVersion() throws Exception { + final GitLab expectedConfiguration = new GitLab("http://fake", ""); + final Mapping mapping = new Mapping(); + mapping.put("repoUrl", "http://fake"); + mapping.put("version", ""); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + + @Test + public void testInstanceWithNullVersion() throws Exception { + // If passing a null, GitLab throws an exception + final GitLab expectedConfiguration = new GitLab("http://fake", ""); + final Mapping mapping = new Mapping(); + mapping.put("repoUrl", "http://fake"); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + + @Test + public void testInstanceWithNullMapping() throws Exception { + // A null mapping should create an instance with empty arguments + final GitLab expectedConfiguration = new GitLab("", ""); + final Mapping mapping = null; + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + + @Test + public void testInstanceWithNaNVersion() throws Exception { + final Mapping mapping = new Mapping(); + mapping.put("repoUrl", "http://fake"); + mapping.put("version", "NaN"); + // When version is NaN, then GitLab uses the DEFAULT_VERSION. It's the same result as using an empty String + final GitLab expectedConfiguration = new GitLab("http://fake", ""); + + final GitLab instance = configurator.instance(mapping, NULL_CONFIGURATION_CONTEXT); + assertEquals(expectedConfiguration.getRepoUrl(), instance.getRepoUrl()); + assertEquals(String.valueOf(expectedConfiguration.getVersion()), String.valueOf(instance.getVersion())); + } + +} diff --git a/src/test/java/hudson/plugins/git/extensions/GitSCMExtensionTest.java b/src/test/java/hudson/plugins/git/extensions/GitSCMExtensionTest.java new file mode 100644 index 0000000000..c929e96d52 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/GitSCMExtensionTest.java @@ -0,0 +1,82 @@ +package hudson.plugins.git.extensions; + +import hudson.model.*; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.TestGitRepo; +import hudson.util.StreamTaskListener; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.JenkinsRule; + +import java.util.Collections; +import java.util.List; + +/** + * @author Kanstantsin Shautsou + */ +public abstract class GitSCMExtensionTest { + + protected TaskListener listener; + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + listener = StreamTaskListener.fromStderr(); + before(); + } + + protected abstract void before() throws Exception; + + /** + * The {@link GitSCMExtension} being tested - this will be added to the + * project built in {@link #setupBasicProject(TestGitRepo)} + * @return the extension + */ + protected abstract GitSCMExtension getExtension(); + + protected FreeStyleBuild build(final FreeStyleProject project, final Result expectedResult) throws Exception { + final FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserIdCause()).get(); + if(expectedResult != null) { + j.assertBuildStatus(expectedResult, build); + } + return build; + } + + /** + * Create a {@link FreeStyleProject} configured with a {@link GitSCM} + * building on the {@code master} branch of the provided {@code repo}, + * and with the extension described in {@link #getExtension()} added. + * @param repo git repository + * @return the created project + * @throws Exception on error + */ + protected FreeStyleProject setupBasicProject(TestGitRepo repo) throws Exception { + GitSCMExtension extension = getExtension(); + FreeStyleProject project = j.createFreeStyleProject("p"); + List branches = Collections.singletonList(new BranchSpec("master")); + GitSCM scm = new GitSCM( + repo.remoteConfigs(), + branches, + false, Collections.emptyList(), + null, null, + Collections.emptyList()); + scm.getExtensions().add(extension); + project.setScm(scm); + project.getBuildersList().add(new CaptureEnvironmentBuilder()); + return project; + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/AuthorInChangelogTest.java b/src/test/java/hudson/plugins/git/extensions/impl/AuthorInChangelogTest.java new file mode 100644 index 0000000000..20a5e32178 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/AuthorInChangelogTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class AuthorInChangelogTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(AuthorInChangelog.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionTest.java new file mode 100644 index 0000000000..4565415f83 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionTest.java @@ -0,0 +1,114 @@ +package hudson.plugins.git.extensions.impl; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CheckoutOptionTest { + + private CheckoutOption option; + private static final int INITIAL_TIMEOUT = 10; + + public CheckoutOptionTest() { + } + + @Before + public void setUp() { + option = new CheckoutOption(INITIAL_TIMEOUT); + } + + @Test + public void testGetTimeout() { + assertEquals(INITIAL_TIMEOUT, (int) option.getTimeout()); + } + + @Test + public void testRequiresWorkspaceForPolling() { + assertFalse(option.requiresWorkspaceForPolling()); + } + + @Test + public void testDecorateCheckoutCommand() throws Exception { + final int NEW_TIMEOUT = 13; + + CheckoutCommandImpl cmd = new CheckoutCommandImpl(); + assertEquals(INITIAL_TIMEOUT, cmd.getTimeout()); + + GitSCM scm = null; + Run build = null; + TaskListener listener = null; + GitClient git = null; + + option = new CheckoutOption(NEW_TIMEOUT); + option.decorateCheckoutCommand(scm, build, git, listener, cmd); + assertEquals(NEW_TIMEOUT, cmd.getTimeout()); + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(CheckoutOption.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } + + public class CheckoutCommandImpl implements CheckoutCommand { + + private int timeout = INITIAL_TIMEOUT; + + public int getTimeout() { + return timeout; + } + + @Override + public CheckoutCommand timeout(Integer timeout) { + this.timeout = timeout; + return this; + } + + @Override + public CheckoutCommand ref(String ref) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public CheckoutCommand branch(String branch) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public CheckoutCommand deleteBranchIfExist(boolean deleteBranch) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public CheckoutCommand sparseCheckoutPaths(List sparseCheckoutPaths) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public CheckoutCommand lfsRemote(String lfsRemote) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public CheckoutCommand lfsCredentials(StandardCredentials lfsCredentials) { + throw new UnsupportedOperationException("Don't call me"); + } + + @Override + public void execute() throws GitException, InterruptedException { + throw new UnsupportedOperationException("Don't call me"); + } + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionWorkflowTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionWorkflowTest.java new file mode 100644 index 0000000000..310083a515 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CheckoutOptionWorkflowTest.java @@ -0,0 +1,32 @@ +package hudson.plugins.git.extensions.impl; + +import jenkins.plugins.git.GitSampleRepoRule; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class CheckoutOptionWorkflowTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + @Test + public void checkoutTimeout() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " checkout(\n" + + " [$class: 'GitSCM', extensions: [[$class: 'CheckoutOption', timeout: 1234]],\n" + + " userRemoteConfigs: [[url: $/" + sampleRepo + "/$]]]\n" + + " )" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("# timeout=1234", b); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckoutTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckoutTest.java new file mode 100644 index 0000000000..84e4582bd4 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CleanBeforeCheckoutTest.java @@ -0,0 +1,16 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Test; + +public class CleanBeforeCheckoutTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(CleanBeforeCheckout.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CleanCheckoutTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CleanCheckoutTest.java new file mode 100644 index 0000000000..13629fbd82 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CleanCheckoutTest.java @@ -0,0 +1,16 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Test; + +public class CleanCheckoutTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(CleanCheckout.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionDepthTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionDepthTest.java new file mode 100644 index 0000000000..13c1e58a39 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionDepthTest.java @@ -0,0 +1,99 @@ +package hudson.plugins.git.extensions.impl; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.PrintStream; + +import hudson.EnvVars; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; +import org.jenkinsci.plugins.gitclient.CloneCommand; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; + +@RunWith(Parameterized.class) +public class CloneOptionDepthTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + private GitSCM scm; + private Run build; + private GitClient git; + private TaskListener listener; + + private final int configuredDepth; + private final int usedDepth; + + public CloneOptionDepthTest(int configuredDepth, int usedDepth) { + this.configuredDepth = configuredDepth; + this.usedDepth = usedDepth; + } + + @Parameterized.Parameters(name = "depth: configured={0}, used={1}") + public static Object[][] depthCombinations() { + return new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 } }; + } + + @Before + public void mockDependencies() throws Exception { + scm = mock(GitSCM.class); + build = mock(Run.class); + git = mock(GitClient.class); + listener = mock(TaskListener.class); + + BuildData buildData = mock(BuildData.class); + buildData.lastBuild = mock(Build.class); + when(build.getEnvironment(listener)).thenReturn(mock(EnvVars.class)); + when(scm.getBuildData(build)).thenReturn(buildData); + } + + @Issue("JENKINS-53050") + @Test + public void decorateCloneCommandShouldUseValidShallowDepth() throws Exception { + CloneCommand cloneCommand = mock(CloneCommand.class, Mockito.RETURNS_SELF); + + PrintStream logger = mock(PrintStream.class); + when(listener.getLogger()).thenReturn(logger); + + CloneOption cloneOption = new CloneOption(true, false, null, null); + cloneOption.setDepth(configuredDepth); + + cloneOption.decorateCloneCommand(scm, build, git, listener, cloneCommand); + + verify(cloneCommand).shallow(true); + verify(cloneCommand).depth(usedDepth); + verify(logger).println("Using shallow clone with depth " + usedDepth); + } + + @Issue("JENKINS-53050") + @Test + public void decorateFetchCommandShouldUseValidShallowDepth() throws Exception { + FetchCommand fetchCommand = mock(FetchCommand.class, Mockito.RETURNS_SELF); + + PrintStream logger = mock(PrintStream.class); + when(listener.getLogger()).thenReturn(logger); + + CloneOption cloneOption = new CloneOption(true, false, null, null); + cloneOption.setDepth(configuredDepth); + + cloneOption.decorateFetchCommand(scm, git, listener, fetchCommand); + + verify(fetchCommand).shallow(true); + verify(fetchCommand).depth(usedDepth); + verify(logger).println("Using shallow fetch with depth " + usedDepth); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionNoTagsTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionNoTagsTest.java new file mode 100644 index 0000000000..d16faaa3d0 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionNoTagsTest.java @@ -0,0 +1,77 @@ +package hudson.plugins.git.extensions.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Set; + +import hudson.model.Result; +import hudson.model.FreeStyleProject; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import hudson.plugins.git.extensions.GitSCMExtension; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Test; + +/** + * @author Ronny Händel + */ +public class CloneOptionNoTagsTest extends GitSCMExtensionTest { + + FreeStyleProject project; + TestGitRepo repo; + + @Override + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + } + + @Override + protected GitSCMExtension getExtension() { + final boolean shallowClone = true; + final boolean dontFetchTags = true; + final String noReference = null; + final Integer noTimeout = null; + return new CloneOption(shallowClone, dontFetchTags, noReference, noTimeout); + } + + @Test + public void cloningShouldNotFetchTags() throws Exception { + + repo.commit("repo-init", repo.johnDoe, "repo0 initial commit"); + repo.tag("v0.0.1", "a tag that should never be fetched"); + + assertTrue("scm polling should detect a change after initial commit", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + assertTrue("there should no tags have been cloned from remote", allTagsInProjectWorkspace().isEmpty()); + } + + @Test + public void detectNoChangeAfterCreatingATag() throws Exception { + + repo.commit("repo-init", repo.johnDoe, "repo0 initial commit"); + + assertTrue("scm polling should detect a change after initial commit", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + repo.tag("v0.0.1", "a tag that should never be fetched"); + + assertFalse("scm polling should not detect a change after creating a tag", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + assertTrue("there should no tags have been fetched from remote", allTagsInProjectWorkspace().isEmpty()); + } + + private Set allTagsInProjectWorkspace() throws IOException, InterruptedException { + GitClient git = Git.with(listener, null).in(project.getWorkspace()).getClient(); + return git.getTagNames("*"); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionShallowDefaultTagsTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionShallowDefaultTagsTest.java new file mode 100644 index 0000000000..f23d808b2d --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionShallowDefaultTagsTest.java @@ -0,0 +1,58 @@ +package hudson.plugins.git.extensions.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import hudson.model.Result; +import hudson.model.FreeStyleProject; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import hudson.plugins.git.extensions.GitSCMExtension; + +import java.io.IOException; +import java.util.Set; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Test; + +/** + * @author Ronny Händel + */ +public class CloneOptionShallowDefaultTagsTest extends GitSCMExtensionTest { + + FreeStyleProject project; + TestGitRepo repo; + + @Override + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + } + + @Override + protected GitSCMExtension getExtension() { + final boolean shallowClone = true; + final String noReference = null; + final Integer noTimeout = null; + return new CloneOption(shallowClone, noReference, noTimeout); + } + + @Test + public void evenShallowCloningFetchesTagsByDefault() throws Exception { + final String tagName = "v0.0.1"; + + repo.commit("repo-init", repo.johnDoe, "repo0 initial commit"); + repo.tag(tagName, "a tag that should be fetched by default"); + + assertTrue("scm polling should detect a change after initial commit", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + assertEquals("tag " + tagName + " should have been cloned from remote", 1, tagsInProjectWorkspaceWithName(tagName).size()); + } + + private Set tagsInProjectWorkspaceWithName(String tagPattern) throws IOException, InterruptedException { + GitClient git = Git.with(listener, null).in(project.getWorkspace()).getClient(); + return git.getTagNames(tagPattern); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionTest.java new file mode 100644 index 0000000000..e53a5b4e96 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/CloneOptionTest.java @@ -0,0 +1,16 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Test; + +public class CloneOptionTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(CloneOption.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/EnforceGitClient.java b/src/test/java/hudson/plugins/git/extensions/impl/EnforceGitClient.java new file mode 100644 index 0000000000..341ae31b5c --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/EnforceGitClient.java @@ -0,0 +1,39 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.Extension; +import hudson.plugins.git.extensions.FakeGitSCMExtension; +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; + +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Enforce JGit Client + */ +public class EnforceGitClient extends FakeGitSCMExtension { + + GitClientType clientType = GitClientType.ANY; + + public EnforceGitClient set(GitClientType type) { + this.clientType = type; + return this; + } + + @Override + public GitClientType getRequiredClient() + { + return clientType; + } + + @DataBoundConstructor + public EnforceGitClient() { + } + + @Extension + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + @Override + public String getDisplayName() { + return "Enforce JGit Client"; + } + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/GitLFSPullTest.java b/src/test/java/hudson/plugins/git/extensions/impl/GitLFSPullTest.java new file mode 100644 index 0000000000..a34b6d30c2 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/GitLFSPullTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class GitLFSPullTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(GitLFSPull.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommitTest.java b/src/test/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommitTest.java new file mode 100644 index 0000000000..c8816c0e9e --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/IgnoreNotifyCommitTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class IgnoreNotifyCommitTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(IgnoreNotifyCommit.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/LocalBranchTest.java b/src/test/java/hudson/plugins/git/extensions/impl/LocalBranchTest.java new file mode 100644 index 0000000000..8b5549e6d5 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/LocalBranchTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class LocalBranchTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(LocalBranch.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/MessageExclusionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/MessageExclusionTest.java new file mode 100644 index 0000000000..42373f9a2e --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/MessageExclusionTest.java @@ -0,0 +1,55 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.model.*; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Kanstantsin Shautsou + * based on {@link hudson.plugins.git.MultipleSCMTest} + */ +public class MessageExclusionTest extends GitSCMExtensionTest { + protected FreeStyleProject project; + protected TestGitRepo repo; + + @Override + protected GitSCMExtension getExtension() { + return new MessageExclusion("(?s).*\\[maven-release-plugin\\].*"); + } + + @Override + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + } + + @Test + public void test() throws Exception { + repo.commit("repo-init", repo.johnDoe, "repo0 initial commit"); + + assertTrue("scm polling should detect a change after initial commit", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + + repo.commit("repo-init", repo.janeDoe, " [maven-release-plugin] excluded message commit"); + + assertFalse("scm polling should not detect excluded message", project.poll(listener).hasChanges()); + + repo.commit("repo-init", repo.janeDoe, "first line in excluded commit\nsecond\nthird [maven-release-plugin]\n"); + + assertFalse("scm polling should not detect multiline message", project.poll(listener).hasChanges()); + + // should be enough, but let's test more + + build(project, Result.SUCCESS); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/PathRestrictionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/PathRestrictionTest.java new file mode 100644 index 0000000000..e35a72e273 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/PathRestrictionTest.java @@ -0,0 +1,191 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.model.FreeStyleProject; + +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import hudson.plugins.git.util.BuildData; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import org.mockito.Mockito; + +// NOTE: isRevExcluded generally returns null instead of false +@RunWith(Enclosed.class) +public class PathRestrictionTest { + + @Ignore("Not a test") + public static class FakePathGitChangeSet extends GitChangeSet { + + private Collection paths; + + public FakePathGitChangeSet(Collection paths) { + super(Collections.emptyList(), false); + this.paths = paths; + } + + @Override + public Collection getAffectedPaths() { + return paths; + } + + @Override + public String getCommitId() { + return "fake123"; + } + } + + public abstract static class PathRestrictionExtensionTest extends GitSCMExtensionTest { + + protected FreeStyleProject project; + protected TestGitRepo repo; + protected BuildData mockBuildData = Mockito.mock(BuildData.class); + + @Override + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + } + + } + + public static class NoRulesTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction(null, null); + } + + @Test + public void test() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt", "bar/bar.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } + + public static class EmptyPathsTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction(".*", null); + } + + @Test + public void test() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>()); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } + + + public static class BasicExcludeTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction(null, "bar.*"); + } + + @Test + public void testMiss() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + + @Test + public void testMatch() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/bar.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } + + public static class BasicIncludeTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction("foo.*", null); + } + + @Test + public void testMatch() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + + @Test + public void testMiss() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/bar.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } + + + public static class MultiExcludeTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction(null, "bar.*\n.*bax"); + } + + @Test + public void testAccept() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt", "foo.foo", "README.mdown"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("docs.txt", "more-docs.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("a/really/long/path/file.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + + @Test + public void testReject() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/bar.txt", "foo.bax"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/docs.txt", "bar/more-docs.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } + + public static class MultiIncludeTest extends PathRestrictionExtensionTest { + + @Override + protected GitSCMExtension getExtension() { + return new PathRestriction("foo.*\nqux.*", null); + } + + @Test + public void testAccept() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt", "something/else"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("foo/foo.txt", "foo.foo", "README.mdown"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("docs.txt", "qux/more-docs.txt"))); + assertNull(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + + @Test + public void testReject() throws Exception { + GitChangeSet commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/bar.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("bar/bar.txt", "bar.bar", "README.mdown"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("docs.txt", "more-docs.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + commit = new FakePathGitChangeSet(new HashSet<>(Arrays.asList("a/really/long/path/file.txt"))); + assertTrue(getExtension().isRevExcluded((hudson.plugins.git.GitSCM) project.getScm(), repo.git, commit, listener, mockBuildData)); + } + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/PreBuildMergeTest.java b/src/test/java/hudson/plugins/git/extensions/impl/PreBuildMergeTest.java new file mode 100644 index 0000000000..7f784ed73a --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/PreBuildMergeTest.java @@ -0,0 +1,93 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.UserMergeOptions; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import hudson.plugins.git.util.BuildData; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author dalvizu + */ +public class PreBuildMergeTest extends GitSCMExtensionTest +{ + private FreeStyleProject project; + + private TestGitRepo repo; + + private String MASTER_FILE = "commitFileBase"; + + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + // make an initial commit to master + repo.commit(MASTER_FILE, repo.johnDoe, "Initial Commit"); + // create integration branch + repo.git.branch("integration"); + } + + @Test + public void testBasicPreMerge() throws Exception { + FreeStyleBuild firstBuild = build(project, Result.SUCCESS); + } + + @Test + public void testFailedMerge() throws Exception { + FreeStyleBuild firstBuild = build(project, Result.SUCCESS); + assertEquals(GitSCM.class, project.getScm().getClass()); + GitSCM gitSCM = (GitSCM)project.getScm(); + BuildData buildData = gitSCM.getBuildData(firstBuild); + assertNotNull("Build data not found", buildData); + assertEquals(firstBuild.getNumber(), buildData.lastBuild.getBuildNumber()); + Revision firstMarked = buildData.lastBuild.getMarked(); + Revision firstRevision = buildData.lastBuild.getRevision(); + assertNotNull(firstMarked); + assertNotNull(firstRevision); + + // pretend we merged and published it successfully + repo.git.deleteBranch("integration"); + repo.git.checkoutBranch("integration", "master"); + repo.commit(MASTER_FILE, "new content on integration branch", repo.johnDoe, repo.johnDoe, "Commit which should fail!"); + repo.git.checkout().ref("master").execute(); + + // make a new commit in master branch, this commit should not merge cleanly! + assertFalse("SCM polling should not detect any more changes after build", project.poll(listener).hasChanges()); + String conflictSha1 = repo.commit(MASTER_FILE, "new content - expect a merge conflict!", repo.johnDoe, repo.johnDoe, "Commit which should fail!"); + assertTrue("SCM polling should detect changes", project.poll(listener).hasChanges()); + + FreeStyleBuild secondBuild = build(project, Result.FAILURE); + assertEquals(secondBuild.getNumber(), gitSCM.getBuildData(secondBuild).lastBuild.getBuildNumber()); + // buildData should mark this as built + assertEquals(conflictSha1, gitSCM.getBuildData(secondBuild).lastBuild.getMarked().getSha1String()); + assertEquals(conflictSha1, gitSCM.getBuildData(secondBuild).lastBuild.getRevision().getSha1String()); + + // Check to see that build data is not corrupted (JENKINS-44037) + assertEquals(firstBuild.getNumber(), gitSCM.getBuildData(firstBuild).lastBuild.getBuildNumber()); + assertEquals(firstMarked, gitSCM.getBuildData(firstBuild).lastBuild.getMarked()); + assertEquals(firstRevision, gitSCM.getBuildData(firstBuild).lastBuild.getRevision()); + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(PreBuildMerge.class) + .usingGetClass() + .verify(); + } + + @Override + protected GitSCMExtension getExtension() { + return new PreBuildMerge(new UserMergeOptions("origin", "integration", "default", + MergeCommand.GitPluginFastForwardMode.FF)); + } + +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/PruneStaleBranchTest.java b/src/test/java/hudson/plugins/git/extensions/impl/PruneStaleBranchTest.java new file mode 100644 index 0000000000..ee7dc7170a --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/PruneStaleBranchTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class PruneStaleBranchTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(PruneStaleBranch.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/SparseCheckoutPathTest.java b/src/test/java/hudson/plugins/git/extensions/impl/SparseCheckoutPathTest.java new file mode 100644 index 0000000000..7a18475882 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/SparseCheckoutPathTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class SparseCheckoutPathTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(SparseCheckoutPath.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionDepthTest.java b/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionDepthTest.java new file mode 100644 index 0000000000..42b2ae9906 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionDepthTest.java @@ -0,0 +1,78 @@ +package hudson.plugins.git.extensions.impl; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.PrintStream; + +import hudson.EnvVars; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; +import org.mockito.Mockito; + +@RunWith(Parameterized.class) +public class SubmoduleOptionDepthTest { + + private GitSCM scm; + private Run build; + private GitClient git; + private TaskListener listener; + + private final int configuredDepth; + private final int usedDepth; + + public SubmoduleOptionDepthTest(int configuredDepth, int usedDepth) { + this.configuredDepth = configuredDepth; + this.usedDepth = usedDepth; + } + + @Parameterized.Parameters(name = "depth: configured={0}, used={1}") + public static Object[][] depthCombinations() { + return new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 } }; + } + + @Before + public void mockDependencies() throws Exception { + scm = mock(GitSCM.class); + build = mock(Run.class); + git = mock(GitClient.class); + listener = mock(TaskListener.class); + + BuildData buildData = mock(BuildData.class); + buildData.lastBuild = mock(Build.class); + when(build.getEnvironment(listener)).thenReturn(mock(EnvVars.class)); + when(scm.getBuildData(build)).thenReturn(buildData); + } + + @Issue("JENKINS-53050") + @Test + public void submoduleUpdateShouldUseValidShallowDepth() throws Exception { + SubmoduleUpdateCommand submoduleUpdate = mock(SubmoduleUpdateCommand.class, Mockito.RETURNS_SELF); + when(git.hasGitModules()).thenReturn(true); + when(git.submoduleUpdate()).thenReturn(submoduleUpdate); + + PrintStream logger = mock(PrintStream.class); + when(listener.getLogger()).thenReturn(logger); + + SubmoduleOption submoduleOption = new SubmoduleOption(false, false, false, null, null, false); + submoduleOption.setShallow(true); + submoduleOption.setDepth(configuredDepth); + + submoduleOption.onCheckoutCompleted(scm, build, git, listener); + + verify(submoduleUpdate).shallow(true); + verify(submoduleUpdate).depth(usedDepth); + verify(logger).println("Using shallow submodule update with depth " + usedDepth); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionTest.java new file mode 100644 index 0000000000..35cf5085df --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/SubmoduleOptionTest.java @@ -0,0 +1,62 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.plugins.git.GitSCM; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.jenkinsci.plugins.gitclient.*; + +import org.junit.Test; + +import java.io.IOException; +import static org.hamcrest.Matchers.*; +import org.jvnet.hudson.test.Issue; + +import static org.junit.Assert.*; + +import hudson.model.Run; +import hudson.plugins.git.GitException; +import hudson.model.TaskListener; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; + +import org.mockito.Mockito; + + +public class SubmoduleOptionTest { + + @Issue("JENKINS-31934") + @Test + public void testSubmoduleUpdateThrowsIOException() throws Exception { + SubmoduleOption submoduleOption = new SubmoduleOption(false, false, false, null, null, false); + + // In order to verify that the submodule option correctly converts + // GitExceptions into IOExceptions, setup a SubmoduleOption, and run + // it's onCheckoutCompleted extension point with a mocked git client + // that always throws an exception. + BuildData buildData = Mockito.mock(BuildData.class); + Build lastBuild = Mockito.mock(Build.class); + GitSCM scm = Mockito.mock(GitSCM.class); + Run build = Mockito.mock(Run.class); + GitClient client = Mockito.mock(GitClient.class); + TaskListener listener = Mockito.mock(TaskListener.class); + buildData.lastBuild = lastBuild; + Mockito.when(scm.getBuildData(build)).thenReturn(buildData); + Mockito.when(client.hasGitModules()).thenReturn(true); + Mockito.when(client.submoduleUpdate()).thenThrow(new GitException("a git exception")); + + try { + submoduleOption.onCheckoutCompleted(scm, build, client, listener); + fail("Expected IOException to be thrown"); + } catch (IOException e) { + assertThat(e.getMessage(), is("Could not perform submodule update")); + } + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(SubmoduleOption.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/UserExclusionTest.java b/src/test/java/hudson/plugins/git/extensions/impl/UserExclusionTest.java new file mode 100644 index 0000000000..d0c1dda03b --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/UserExclusionTest.java @@ -0,0 +1,54 @@ +package hudson.plugins.git.extensions.impl; + +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.plugins.git.TestGitRepo; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionTest; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Kanstantsin Shautsou + */ +public class UserExclusionTest extends GitSCMExtensionTest{ + + FreeStyleProject project; + TestGitRepo repo; + + @Override + public void before() throws Exception { + repo = new TestGitRepo("repo", tmp.newFolder(), listener); + project = setupBasicProject(repo); + } + + @Override + protected GitSCMExtension getExtension() { + return new UserExclusion("Jane Doe"); + } + + @Test + public void test() throws Exception { + + repo.commit("repo-init", repo.johnDoe, "repo0 initial commit"); + + assertTrue("scm polling should detect a change after initial commit", project.poll(listener).hasChanges()); + + build(project, Result.SUCCESS); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + + repo.commit("repo-init", repo.janeDoe, "excluded user commit"); + + assertFalse("scm polling should ignore excluded user", project.poll(listener).hasChanges()); + + // should be enough, but let's test more + + build(project, Result.SUCCESS); + + assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); + + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/UserIdentityTest.java b/src/test/java/hudson/plugins/git/extensions/impl/UserIdentityTest.java new file mode 100644 index 0000000000..c636a43ff6 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/UserIdentityTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class UserIdentityTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(UserIdentity.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/extensions/impl/WipeWorkspaceTest.java b/src/test/java/hudson/plugins/git/extensions/impl/WipeWorkspaceTest.java new file mode 100644 index 0000000000..925518b546 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/WipeWorkspaceTest.java @@ -0,0 +1,14 @@ +package hudson.plugins.git.extensions.impl; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class WipeWorkspaceTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(WipeWorkspace.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/opt/PreBuildMergeOptionsTest.java b/src/test/java/hudson/plugins/git/opt/PreBuildMergeOptionsTest.java new file mode 100644 index 0000000000..e403634409 --- /dev/null +++ b/src/test/java/hudson/plugins/git/opt/PreBuildMergeOptionsTest.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * + * Copyright 2014 Jesse Glick. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.plugins.git.opt; + +import hudson.model.FreeStyleProject; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserMergeOptions; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.PreBuildMerge; +import java.util.Collections; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.junit.Test; +import org.junit.Rule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class PreBuildMergeOptionsTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + + @Issue("JENKINS-9843") + @Test public void exporting() throws Exception { + FreeStyleProject p = r.createFreeStyleProject(); + p.setScm(new GitSCM(Collections.singletonList(new UserRemoteConfig("http://wherever/thing.git", "repo", null, null)), null, null, null, null, null, Collections.singletonList(new PreBuildMerge(new UserMergeOptions("repo", "master", MergeCommand.Strategy.DEFAULT.name(), MergeCommand.GitPluginFastForwardMode.FF))))); + r.createWebClient().goToXml(p.getUrl() + "api/xml?depth=2"); + } + +} diff --git a/src/test/java/hudson/plugins/git/util/AncestryBuildChooserTest.java b/src/test/java/hudson/plugins/git/util/AncestryBuildChooserTest.java new file mode 100644 index 0000000000..d84c8f7ddc --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/AncestryBuildChooserTest.java @@ -0,0 +1,251 @@ +package hudson.plugins.git.util; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.AbstractGitRepository; +import hudson.plugins.git.Branch; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.jenkinsci.plugins.gitclient.GitClient; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import org.junit.Test; +import org.mockito.Mockito; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; +import static org.junit.Assert.*; +import org.junit.Before; + +public class AncestryBuildChooserTest extends AbstractGitRepository { + + private String rootCommit = null; + private String ancestorCommit = null; + private String fiveDaysAgoCommit = null; + private String tenDaysAgoCommit = null; + private String twentyDaysAgoCommit = null; + + private final LocalDateTime fiveDaysAgo = LocalDate.now().atStartOfDay().minusDays(5); + private final LocalDateTime tenDaysAgo = LocalDate.now().atStartOfDay().minusDays(10); + private final LocalDateTime twentyDaysAgo = LocalDate.now().atStartOfDay().minusDays(20); + + private final PersonIdent johnDoe = new PersonIdent("John Doe", "john@example.com"); + + /* + * 20 days old -> O O <- 10 days old + * |/ + * ancestor -> O O <- 5 days old + * \ / + * root -> O + * + * Creates a small repository of 5 commits with different branches and ages. + */ + @Before + public void setUp() throws Exception { + Set prevBranches = stringifyBranches(testGitClient.getBranches()); + + testGitClient.commit("Root Commit"); + rootCommit = getLastCommitSha1(prevBranches); + + testGitClient.commit("Ancestor Commit"); + ancestorCommit = getLastCommitSha1(prevBranches); + + testGitClient.branch("20-days-old-branch"); + testGitClient.checkoutBranch("20-days-old-branch", ancestorCommit); + Date twentyDaysAgoDate = Date.from(twentyDaysAgo.atZone(ZoneId.systemDefault()).toInstant()); + PersonIdent johnDoeTwentyDaysAgo = new PersonIdent(johnDoe, twentyDaysAgoDate); + this.commit("20 days ago commit message", johnDoeTwentyDaysAgo, johnDoeTwentyDaysAgo); + twentyDaysAgoCommit = getLastCommitSha1(prevBranches); + + testGitClient.checkout().ref(ancestorCommit).execute(); + testGitClient.checkoutBranch("10-days-old-branch", ancestorCommit); + Date tenDaysAgoDate = Date.from(tenDaysAgo.atZone(ZoneId.systemDefault()).toInstant()); + PersonIdent johnDoeTenDaysAgo = new PersonIdent(johnDoe, tenDaysAgoDate); + this.commit("10 days ago commit message", johnDoeTenDaysAgo, johnDoeTenDaysAgo); + tenDaysAgoCommit = getLastCommitSha1(prevBranches); + + testGitClient.checkout().ref(rootCommit).execute(); + testGitClient.checkoutBranch("5-days-old-branch", rootCommit); + Date fiveDaysAgoDate = Date.from(fiveDaysAgo.atZone(ZoneId.systemDefault()).toInstant()); + PersonIdent johnDoeFiveDaysAgo = new PersonIdent(johnDoe, fiveDaysAgoDate); + this.commit("5 days ago commit message", johnDoeFiveDaysAgo, johnDoeFiveDaysAgo); + fiveDaysAgoCommit = getLastCommitSha1(prevBranches); + } + + private Set stringifyBranches(Set original) { + Set result = new TreeSet<>(); + + for (Iterator iter = original.iterator(); iter.hasNext(); ) { + result.add(iter.next().getSHA1String()); + } + + return result; + } + + private String getLastCommitSha1(Set prevBranches) throws Exception { + Set newBranches = stringifyBranches(testGitClient.getBranches()); + + SetView difference = Sets.difference(newBranches, prevBranches); + + assertEquals(1, difference.size()); + + String result = difference.iterator().next(); + + prevBranches.clear(); + prevBranches.addAll(newBranches); + + return result; + } + + // Git Client implementation throws away committer date info so we have to do this manually.. + // Copied from JGitAPIImpl.commit(String message) + private void commit(String message, PersonIdent author, PersonIdent committer) { + try (Repository repo = testGitClient.getRepository()) { + CommitCommand cmd = Git.wrap(repo).commit().setMessage(message); + if (author != null) + cmd.setAuthor(author); + if (committer != null) + // cmd.setCommitter(new PersonIdent(committer,new Date())); + cmd.setCommitter(committer); + cmd.call(); + } catch (GitAPIException e) { + throw new GitException(e); + } + } + + private List getFilteredTestCandidates(Integer maxAgeInDays, String ancestorCommitSha1) throws Exception { + GitSCM gitSCM = new GitSCM("foo"); + AncestryBuildChooser chooser = new AncestryBuildChooser(maxAgeInDays, ancestorCommitSha1); + gitSCM.getExtensions().add(new BuildChooserSetting(chooser)); + assertEquals(maxAgeInDays, chooser.getMaximumAgeInDays()); + assertEquals(ancestorCommitSha1, chooser.getAncestorCommitSha1()); + + // mock necessary objects + GitClient git = Mockito.spy(this.testGitClient); + Mockito.when(git.getRemoteBranches()).thenReturn(this.testGitClient.getBranches()); + + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.hasBeenBuilt(git.revParse(rootCommit))).thenReturn(false); + + BuildChooserContext context = Mockito.mock(BuildChooserContext.class); + Mockito.when(context.getEnvironment()).thenReturn(new EnvVars()); + + TaskListener listener = TaskListener.NULL; + + // get filtered candidates + Collection candidateRevisions = gitSCM.getBuildChooser().getCandidateRevisions(true, "**-days-old-branch", git, listener, buildData, context); + + // transform revision candidates to sha1 strings + List candidateSha1s = Lists.newArrayList(Iterables.transform(candidateRevisions, new Function() { + public String apply(Revision rev) { + return rev.getSha1String(); + } + })); + + return candidateSha1s; + } + + @Test + public void testFilterRevisionsNoRestriction() throws Exception { + final Integer maxAgeInDays = null; + final String ancestorCommitSha1 = null; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(3, candidateSha1s.size()); + assertTrue(candidateSha1s.contains(fiveDaysAgoCommit)); + assertTrue(candidateSha1s.contains(tenDaysAgoCommit)); + assertTrue(candidateSha1s.contains(twentyDaysAgoCommit)); + } + + @Test + public void testFilterRevisionsZeroDate() throws Exception { + final Integer maxAgeInDays = 0; + final String ancestorCommitSha1 = null; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(0, candidateSha1s.size()); + } + + @Test + public void testFilterRevisionsTenDays() throws Exception { + final Integer maxAgeInDays = 10; + final String ancestorCommitSha1 = null; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(1, candidateSha1s.size()); + assertTrue(candidateSha1s.contains(fiveDaysAgoCommit)); + } + + @Test + public void testFilterRevisionsThirtyDays() throws Exception { + final Integer maxAgeInDays = 30; + final String ancestorCommitSha1 = null; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(3, candidateSha1s.size()); + assertTrue(candidateSha1s.contains(fiveDaysAgoCommit)); + assertTrue(candidateSha1s.contains(tenDaysAgoCommit)); + assertTrue(candidateSha1s.contains(twentyDaysAgoCommit)); + } + + @Test + public void testFilterRevisionsBlankAncestor() throws Exception { + final Integer maxAgeInDays = null; + final String ancestorCommitSha1 = ""; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(3, candidateSha1s.size()); + assertTrue(candidateSha1s.contains(fiveDaysAgoCommit)); + assertTrue(candidateSha1s.contains(tenDaysAgoCommit)); + assertTrue(candidateSha1s.contains(twentyDaysAgoCommit)); + } + + @Test + public void testFilterRevisionsNonExistingAncestor() throws Exception { + final Integer maxAgeInDays = null; + final String ancestorCommitSha1 = "This commit sha1 does not exist."; + + try { + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + fail("Invalid sha1 should throw GitException."); + } catch (GitException e) { + return; + } + } + + @Test + public void testFilterRevisionsExistingAncestor() throws Exception { + final Integer maxAgeInDays = null; + final String ancestorCommitSha1 = ancestorCommit; + + List candidateSha1s = getFilteredTestCandidates(maxAgeInDays, ancestorCommitSha1); + + assertEquals(2, candidateSha1s.size()); + assertTrue(candidateSha1s.contains(tenDaysAgoCommit)); + assertTrue(candidateSha1s.contains(twentyDaysAgoCommit)); + } +} diff --git a/src/test/java/hudson/plugins/git/util/BuildDataLoggingTest.java b/src/test/java/hudson/plugins/git/util/BuildDataLoggingTest.java new file mode 100644 index 0000000000..7daf5e7590 --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/BuildDataLoggingTest.java @@ -0,0 +1,86 @@ +package hudson.plugins.git.util; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * @author Mark Waite + */ +public class BuildDataLoggingTest { + + private BuildData data; + + private final Level ORIGINAL_LEVEL = BuildData.LOGGER.getLevel(); + private final boolean ORIGINAL_USE_PARENT_HANDLERS = BuildData.LOGGER.getUseParentHandlers(); + + private LogHandler handler = null; + + @Before + public void createTestData() throws Exception { + data = new BuildData(); + } + + @Before + public void reconfigureLogging() throws Exception { + handler = new LogHandler(); + handler.setLevel(Level.ALL); + BuildData.LOGGER.setUseParentHandlers(false); + BuildData.LOGGER.addHandler(handler); + BuildData.LOGGER.setLevel(Level.ALL); + } + + @After + public void restoreLogging() throws Exception { + BuildData.LOGGER.removeHandler(handler); + BuildData.LOGGER.setUseParentHandlers(ORIGINAL_USE_PARENT_HANDLERS); + BuildData.LOGGER.setLevel(ORIGINAL_LEVEL); + } + + /* Confirm URISyntaxException is logged at FINEST on invalid URL */ + @Test + public void testSimilarToInvalidHttpsRemoteURL() { + final String INVALID_URL = "https://github.com/jenkinsci/git-plugin?s=^IXIC"; + BuildData invalid = new BuildData(); + invalid.addRemoteUrl(INVALID_URL); + assertTrue("Invalid URL not similar to itself " + INVALID_URL, invalid.similarTo(invalid)); + + String expectedMessage = "URI syntax exception on " + INVALID_URL; + assertThat(handler.checkMessage(), is(expectedMessage)); + assertThat(handler.checkLevel(), is(Level.FINEST)); + } + + class LogHandler extends Handler { + + private Level lastLevel = Level.INFO; + private String lastMessage = ""; + + public Level checkLevel() { + return lastLevel; + } + + public String checkMessage() { + return lastMessage; + } + + @Override + public void publish(LogRecord record) { + lastLevel = record.getLevel(); + lastMessage = record.getMessage(); + } + + @Override + public void close() { + } + + @Override + public void flush() { + } + } +} diff --git a/src/test/java/hudson/plugins/git/util/BuildDataTest.java b/src/test/java/hudson/plugins/git/util/BuildDataTest.java index 28b5d88235..f6bf2ce9a5 100644 --- a/src/test/java/hudson/plugins/git/util/BuildDataTest.java +++ b/src/test/java/hudson/plugins/git/util/BuildDataTest.java @@ -1,27 +1,494 @@ package hudson.plugins.git.util; -import hudson.plugins.git.AbstractGitTestCase; +import hudson.model.Api; +import hudson.model.Result; +import hudson.plugins.git.Branch; +import hudson.plugins.git.Revision; +import hudson.plugins.git.UserRemoteConfig; -import hudson.plugins.git.util.BuildData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.eclipse.jgit.lib.ObjectId; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; /** * @author Mark Waite */ -public class BuildDataTest extends AbstractGitTestCase { - /** - * Verifies that the display name is "Git Build Data". - */ - public void testDisplayNameWithoutSCM() throws Exception { - final BuildData data = new BuildData(); - assertEquals(data.getDisplayName(), "Git Build Data"); - } - - /** - * Verifies that the display name is "Git Build Data:scmName". - */ - public void testDisplayNameWithSCM() throws Exception { +public class BuildDataTest { + + private BuildData data; + private final ObjectId sha1 = ObjectId.fromString("929e92e3adaff2e6e1d752a8168c1598890fe84c"); + private final String remoteUrl = "https://github.com/jenkinsci/git-plugin"; + + @Before + public void setUp() throws Exception { + data = new BuildData(); + } + + @Test + public void testGetDisplayName() throws Exception { + assertThat(data.getDisplayName(), is("Git Build Data")); + } + + @Test + public void testGetDisplayNameEmptyString() throws Exception { + String scmName = ""; + BuildData dataWithSCM = new BuildData(scmName); + assertThat(dataWithSCM.getDisplayName(), is("Git Build Data")); + } + + @Test + public void testGetDisplayNameNullSCMName() throws Exception { + BuildData dataWithNullSCM = new BuildData(null); + assertThat(dataWithNullSCM.getDisplayName(), is("Git Build Data")); + } + + @Test + public void testGetDisplayNameWithSCM() throws Exception { final String scmName = "testSCM"; - final BuildData data = new BuildData(scmName); - assertEquals("Git Build Data:" + scmName, data.getDisplayName()); + final BuildData dataWithSCM = new BuildData(scmName); + assertThat("Git Build Data:" + scmName, is(dataWithSCM.getDisplayName())); + } + + @Test + public void testGetIconFileName() { + assertThat(data.getIconFileName(), endsWith("/plugin/git/icons/git-32x32.png")); + } + + @Test + public void testGetUrlName() { + assertThat(data.getUrlName(), is("git")); + } + + @Test + public void testGetUrlNameMultipleEntries() { + Random random = new Random(); + int randomIndex = random.nextInt(1234) + 1; + data.setIndex(randomIndex); + assertThat(data.getUrlName(), is("git-" + randomIndex)); + } + + @Test + public void testHasBeenBuilt() { + assertFalse(data.hasBeenBuilt(sha1)); + } + + @Test + public void testGetLastBuild() { + assertEquals(null, data.getLastBuild(sha1)); + } + + @Test + public void testSaveBuild() { + Revision revision = new Revision(sha1); + Build build = new Build(revision, 1, Result.SUCCESS); + data.saveBuild(build); + assertThat(data.getLastBuild(sha1), is(build)); + } + + @Test + public void testGetLastBuildOfBranch() { + String branchName = "origin/master"; + assertEquals(null, data.getLastBuildOfBranch(branchName)); + + Collection branches = new ArrayList<>(); + Branch branch = new Branch(branchName, sha1); + branches.add(branch); + Revision revision = new Revision(sha1, branches); + Build build = new Build(revision, 13, Result.FAILURE); + data.saveBuild(build); + assertThat(data.getLastBuildOfBranch(branchName), is(build)); + } + + @Test + public void testGetLastBuiltRevision() { + Revision revision = new Revision(sha1); + Build build = new Build(revision, 1, Result.SUCCESS); + data.saveBuild(build); + assertThat(data.getLastBuiltRevision(), is(revision)); + } + + @Test + public void testGetBuildsByBranchName() { + assertTrue(data.getBuildsByBranchName().isEmpty()); + } + + @Test + public void testGetScmName() { + assertThat(data.getScmName(), is("")); + } + + @Test + public void testSetScmName() { + final String scmName = "Some SCM name"; + data.setScmName(scmName); + assertThat(data.getScmName(), is(scmName)); + } + + @Test + public void testAddRemoteUrl() { + data.addRemoteUrl(remoteUrl); + assertEquals(1, data.getRemoteUrls().size()); + + String remoteUrl2 = "https://github.com/jenkinsci/git-plugin.git/"; + data.addRemoteUrl(remoteUrl2); + assertFalse(data.getRemoteUrls().isEmpty()); + assertTrue("Second URL not found in remote URLs", data.getRemoteUrls().contains(remoteUrl2)); + assertEquals(2, data.getRemoteUrls().size()); + } + + @Test + public void testHasBeenReferenced() { + assertFalse(data.hasBeenReferenced(remoteUrl)); + data.addRemoteUrl(remoteUrl); + assertTrue(data.hasBeenReferenced(remoteUrl)); + assertFalse(data.hasBeenReferenced(remoteUrl + "/")); + } + + @Test + public void testGetApi() { + Api api = data.getApi(); + Api apiClone = data.clone().getApi(); + assertEquals(api, api); + assertEquals(api.getSearchUrl(), apiClone.getSearchUrl()); + } + + @Test + public void testToString() { + assertEquals(data.toString(), data.clone().toString()); + } + + @Test + public void testToStringEmptyBuildData() { + BuildData empty = new BuildData(); + assertThat(empty.toString(), endsWith("[scmName=,remoteUrls=[],buildsByBranchName={},lastBuild=null]")); + } + + @Test + public void testToStringNullSCMBuildData() { + BuildData nullSCM = new BuildData(null); + assertThat(nullSCM.toString(), endsWith("[scmName=,remoteUrls=[],buildsByBranchName={},lastBuild=null]")); + } + + @Test + public void testToStringNonNullSCMBuildData() { + BuildData nonNullSCM = new BuildData("gitless"); + assertThat(nonNullSCM.toString(), endsWith("[scmName=gitless,remoteUrls=[],buildsByBranchName={},lastBuild=null]")); + } + + @Test + public void testEquals() { + // Null object not equal non-null + BuildData nullData = null; + assertFalse("Null object not equal non-null", data.equals(nullData)); + + // Object should equal itself + assertEquals("Object not equal itself", data, data); + assertTrue("Object not equal itself", data.equals(data)); + assertEquals("Object hashCode not equal itself", data.hashCode(), data.hashCode()); + + // Cloned object equals original object + BuildData data1 = data.clone(); + assertEquals("Cloned objects not equal", data1, data); + assertTrue("Cloned objects not equal", data1.equals(data)); + assertTrue("Cloned objects not equal", data.equals(data1)); + assertEquals("Cloned object hashCodes not equal", data.hashCode(), data1.hashCode()); + + // Saved build makes object unequal + Revision revision1 = new Revision(sha1); + Build build1 = new Build(revision1, 1, Result.SUCCESS); + data1.saveBuild(build1); + assertFalse("Distinct objects shouldn't be equal", data.equals(data1)); + assertFalse("Distinct objects shouldn't be equal", data1.equals(data)); + + // Same saved build makes objects equal + BuildData data2 = data.clone(); + data2.saveBuild(build1); + assertTrue("Objects with same saved build not equal", data2.equals(data1)); + assertTrue("Objects with same saved build not equal", data1.equals(data2)); + assertEquals("Objects with same saved build not equal hashCodes", data2.hashCode(), data1.hashCode()); + + // Add remote URL makes objects unequal + final String remoteUrl2 = "git://github.com/jenkinsci/git-plugin.git"; + data1.addRemoteUrl(remoteUrl2); + assertFalse("Distinct objects shouldn't be equal", data.equals(data1)); + assertFalse("Distinct objects shouldn't be equal", data1.equals(data)); + + // Add same remote URL makes objects equal + data2.addRemoteUrl(remoteUrl2); + assertTrue("Objects with same remote URL not equal", data2.equals(data1)); + assertTrue("Objects with same remote URL not equal", data1.equals(data2)); + assertEquals("Objects with same remote URL not equal hashCodes", data2.hashCode(), data1.hashCode()); + + // Another saved build still keeps objects equal + String branchName = "origin/master"; + Collection branches = new ArrayList<>(); + Branch branch = new Branch(branchName, sha1); + branches.add(branch); + Revision revision2 = new Revision(sha1, branches); + Build build2 = new Build(revision2, 1, Result.FAILURE); + assertEquals(build1, build2); // Surprising, since build1 result is SUCCESS, build2 result is FAILURE + data1.saveBuild(build2); + data2.saveBuild(build2); + assertTrue(data1.equals(data2)); + assertEquals(data1.hashCode(), data2.hashCode()); + + // Saving different build results still equal BuildData, + // because the different build results are equal + data1.saveBuild(build1); + data2.saveBuild(build2); + assertTrue(data1.equals(data2)); + assertEquals(data1.hashCode(), data2.hashCode()); + + // Set SCM name doesn't change equality or hashCode + data1.setScmName("scm 1"); + assertTrue(data1.equals(data2)); + assertEquals(data1.hashCode(), data2.hashCode()); + data2.setScmName("scm 2"); + assertTrue(data1.equals(data2)); + assertEquals(data1.hashCode(), data2.hashCode()); + + BuildData emptyData = new BuildData(); + emptyData.remoteUrls = null; + assertNotEquals("Non-empty object equal empty", data, emptyData); + assertNotEquals("Empty object similar to non-empty", emptyData, data); + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(BuildData.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .withIgnoredFields("index", "scmName") + .verify(); + } + + @Test + public void testSetIndex() { + data.setIndex(null); + assertEquals(null, data.getIndex()); + data.setIndex(-1); + assertEquals(null, data.getIndex()); + data.setIndex(0); + assertEquals(null, data.getIndex()); + data.setIndex(13); + assertEquals(13, data.getIndex().intValue()); + data.setIndex(-1); + assertEquals(null, data.getIndex()); + } + + @Test + public void testSimilarToHttpsRemoteURL() { + final String SIMPLE_URL = "https://github.com/jenkinsci/git-plugin"; + BuildData simple = new BuildData("git-" + SIMPLE_URL); + simple.addRemoteUrl(SIMPLE_URL); + permuteBaseURL(SIMPLE_URL, simple); + } + + @Test + public void testSimilarToScpRemoteURL() { + final String SIMPLE_URL = "git@github.com:jenkinsci/git-plugin"; + BuildData simple = new BuildData("git-" + SIMPLE_URL); + simple.addRemoteUrl(SIMPLE_URL); + permuteBaseURL(SIMPLE_URL, simple); + } + + @Test + public void testSimilarToSshRemoteURL() { + final String SIMPLE_URL = "ssh://git@github.com/jenkinsci/git-plugin"; + BuildData simple = new BuildData("git-" + SIMPLE_URL); + simple.addRemoteUrl(SIMPLE_URL); + permuteBaseURL(SIMPLE_URL, simple); + } + + private void permuteBaseURL(String simpleURL, BuildData simple) { + final String TRAILING_SLASH_URL = simpleURL + "/"; + BuildData trailingSlash = new BuildData("git-" + TRAILING_SLASH_URL); + trailingSlash.addRemoteUrl(TRAILING_SLASH_URL); + assertTrue("Trailing slash not similar to simple URL " + TRAILING_SLASH_URL, + trailingSlash.similarTo(simple)); + + final String TRAILING_SLASHES_URL = TRAILING_SLASH_URL + "//"; + BuildData trailingSlashes = new BuildData("git-" + TRAILING_SLASHES_URL); + trailingSlashes.addRemoteUrl(TRAILING_SLASHES_URL); + assertTrue("Trailing slashes not similar to simple URL " + TRAILING_SLASHES_URL, + trailingSlashes.similarTo(simple)); + + final String DOT_GIT_URL = simpleURL + ".git"; + BuildData dotGit = new BuildData("git-" + DOT_GIT_URL); + dotGit.addRemoteUrl(DOT_GIT_URL); + assertTrue("Dot git not similar to simple URL " + DOT_GIT_URL, + dotGit.similarTo(simple)); + + final String DOT_GIT_TRAILING_SLASH_URL = DOT_GIT_URL + "/"; + BuildData dotGitTrailingSlash = new BuildData("git-" + DOT_GIT_TRAILING_SLASH_URL); + dotGitTrailingSlash.addRemoteUrl(DOT_GIT_TRAILING_SLASH_URL); + assertTrue("Dot git trailing slash not similar to dot git URL " + DOT_GIT_TRAILING_SLASH_URL, + dotGitTrailingSlash.similarTo(dotGit)); + + final String DOT_GIT_TRAILING_SLASHES_URL = DOT_GIT_TRAILING_SLASH_URL + "///"; + BuildData dotGitTrailingSlashes = new BuildData("git-" + DOT_GIT_TRAILING_SLASHES_URL); + dotGitTrailingSlashes.addRemoteUrl(DOT_GIT_TRAILING_SLASHES_URL); + assertTrue("Dot git trailing slashes not similar to dot git URL " + DOT_GIT_TRAILING_SLASHES_URL, + dotGitTrailingSlashes.similarTo(dotGit)); + } + + @Test + @Issue("JENKINS-43630") + public void testSimilarToContainsNullURL() { + final String SIMPLE_URL = "ssh://git@github.com/jenkinsci/git-plugin"; + BuildData simple = new BuildData("git-" + SIMPLE_URL); + simple.addRemoteUrl(SIMPLE_URL); + simple.addRemoteUrl(null); + simple.addRemoteUrl(SIMPLE_URL); + + BuildData simple2 = simple.clone(); + assertTrue(simple.similarTo(simple2)); + + BuildData simple3 = new BuildData("git-" + SIMPLE_URL); + simple3.addRemoteUrl(SIMPLE_URL); + simple3.addRemoteUrl(null); + simple3.addRemoteUrl(SIMPLE_URL); + assertTrue(simple.similarTo(simple3)); + } + + @Test + public void testGetIndex() { + assertEquals(null, data.getIndex()); + } + + @Test + public void testGetRemoteUrls() { + assertTrue(data.getRemoteUrls().isEmpty()); + } + + @Test + public void testClone() { + // Tested in testSimilarTo and testEquals + } + + @Test + public void testSimilarTo() { + data.addRemoteUrl(remoteUrl); + + // Null object not similar to non-null + BuildData dataNull = null; + assertFalse("Null object similar to non-null", data.similarTo(dataNull)); + + BuildData emptyData = new BuildData(); + assertFalse("Non-empty object similar to empty", data.similarTo(emptyData)); + assertFalse("Empty object similar to non-empty", emptyData.similarTo(data)); + emptyData.remoteUrls = null; + assertFalse("Non-empty object similar to empty", data.similarTo(emptyData)); + assertFalse("Empty object similar to non-empty", emptyData.similarTo(data)); + + // Object should be similar to itself + assertTrue("Object not similar to itself", data.similarTo(data)); + + // Object should not be similar to constructed variants + Collection emptyList = new ArrayList<>(); + assertFalse("Object similar to data with SCM name", data.similarTo(new BuildData("abc"))); + assertFalse("Object similar to data with SCM name & empty", data.similarTo(new BuildData("abc", emptyList))); + + BuildData dataSCM = new BuildData("scm"); + assertFalse("Object similar to data with SCM name", dataSCM.similarTo(data)); + assertTrue("Object with SCM name not similar to data with SCM name", dataSCM.similarTo(new BuildData("abc"))); + assertTrue("Object with SCM name not similar to data with SCM name & empty", dataSCM.similarTo(new BuildData("abc", emptyList))); + + // Cloned object equals original object + BuildData dataClone = data.clone(); + assertTrue("Clone not similar to origin", dataClone.similarTo(data)); + assertTrue("Origin not similar to clone", data.similarTo(dataClone)); + + // Saved build makes objects dissimilar + Revision revision1 = new Revision(sha1); + Build build1 = new Build(revision1, 1, Result.SUCCESS); + dataClone.saveBuild(build1); + assertFalse("Unmodified origin similar to modified clone", data.similarTo(dataClone)); + assertFalse("Modified clone similar to unmodified origin", dataClone.similarTo(data)); + assertTrue("Modified clone not similar to itself", dataClone.similarTo(dataClone)); + + // Same saved build makes objects similar + BuildData data2 = data.clone(); + data2.saveBuild(build1); + assertFalse("Unmodified origin similar to modified clone", data.similarTo(data2)); + assertTrue("Objects with same saved build not similar (1)", data2.similarTo(dataClone)); + assertTrue("Objects with same saved build not similar (2)", dataClone.similarTo(data2)); + + // Add remote URL makes objects dissimilar + final String remoteUrl = "git://github.com/jenkinsci/git-client-plugin.git"; + dataClone.addRemoteUrl(remoteUrl); + assertFalse("Distinct objects shouldn't be similar (1)", data.similarTo(dataClone)); + assertFalse("Distinct objects shouldn't be similar (2)", dataClone.similarTo(data)); + + // Add same remote URL makes objects similar + data2.addRemoteUrl(remoteUrl); + assertTrue("Objects with same remote URL dissimilar", data2.similarTo(dataClone)); + assertTrue("Objects with same remote URL dissimilar", dataClone.similarTo(data2)); + + // Add different remote URL objects similar + final String trailingSlash = "git-client-plugin.git/"; // Unlikely as remote URL + dataClone.addRemoteUrl(trailingSlash); + assertFalse("Distinct objects shouldn't be similar", data.similarTo(dataClone)); + assertFalse("Distinct objects shouldn't be similar", dataClone.similarTo(data)); + + data2.addRemoteUrl(trailingSlash); + assertTrue("Objects with same remote URL dissimilar", data2.similarTo(dataClone)); + assertTrue("Objects with same remote URL dissimilar", dataClone.similarTo(data2)); + + // Add different remote URL objects + final String noSlash = "git-client-plugin"; // Unlikely as remote URL + dataClone.addRemoteUrl(noSlash); + assertFalse("Distinct objects shouldn't be similar", data.similarTo(dataClone)); + assertFalse("Distinct objects shouldn't be similar", dataClone.similarTo(data)); + + data2.addRemoteUrl(noSlash); + assertTrue("Objects with same remote URL dissimilar", data2.similarTo(dataClone)); + assertTrue("Objects with same remote URL dissimilar", dataClone.similarTo(data2)); + + // Another saved build still keeps objects similar + String branchName = "origin/master"; + Collection branches = new ArrayList<>(); + Branch branch = new Branch(branchName, sha1); + branches.add(branch); + Revision revision2 = new Revision(sha1, branches); + Build build2 = new Build(revision2, 1, Result.FAILURE); + dataClone.saveBuild(build2); + assertTrue("Another saved build, still similar (1)", dataClone.similarTo(data2)); + assertTrue("Another saved build, still similar (2)", data2.similarTo(dataClone)); + data2.saveBuild(build2); + assertTrue("Another saved build, still similar (3)", dataClone.similarTo(data2)); + assertTrue("Another saved build, still similar (4)", data2.similarTo(dataClone)); + + // Saving different build results still similar BuildData + dataClone.saveBuild(build1); + assertTrue("Saved build with different results, similar (5)", dataClone.similarTo(data2)); + assertTrue("Saved build with different results, similar (6)", data2.similarTo(dataClone)); + data2.saveBuild(build2); + assertTrue("Saved build with different results, similar (7)", dataClone.similarTo(data2)); + assertTrue("Saved build with different results, similar (8)", data2.similarTo(dataClone)); + + // Set SCM name doesn't change similarity + dataClone.setScmName("scm 1"); + assertTrue(dataClone.similarTo(data2)); + data2.setScmName("scm 2"); + assertTrue(dataClone.similarTo(data2)); + } + + @Test + public void testHashCodeEmptyData() { + BuildData emptyData = new BuildData(); + assertEquals(emptyData.hashCode(), emptyData.hashCode()); + emptyData.remoteUrls = null; + assertEquals(emptyData.hashCode(), emptyData.hashCode()); } } diff --git a/src/test/java/hudson/plugins/git/util/BuildTest.java b/src/test/java/hudson/plugins/git/util/BuildTest.java new file mode 100644 index 0000000000..1ed1824b81 --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/BuildTest.java @@ -0,0 +1,17 @@ +package hudson.plugins.git.util; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Test; + +public class BuildTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(Build.class) + .usingGetClass() + .suppress(Warning.NONFINAL_FIELDS) + .withIgnoredFields("hudsonBuildResult") + .verify(); + } +} diff --git a/src/test/java/hudson/plugins/git/util/CandidateRevisionsTest.java b/src/test/java/hudson/plugins/git/util/CandidateRevisionsTest.java new file mode 100644 index 0000000000..33a3ee8a9d --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/CandidateRevisionsTest.java @@ -0,0 +1,124 @@ +package hudson.plugins.git.util; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.AbstractGitRepository; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.Random; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.RefSpec; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import jenkins.plugins.git.GitSampleRepoRule; +import org.mockito.Mockito; + +public class CandidateRevisionsTest extends AbstractGitRepository { + + private File testGitDir2; + private GitClient testGitClient2; + + @Rule + public GitSampleRepoRule testGitRepo2 = new GitSampleRepoRule(); + + @Before + public void createSecondGitRepository() throws Exception { + testGitRepo2.init(); + testGitDir2 = testGitRepo2.getRoot(); + TaskListener listener = StreamTaskListener.fromStderr(); + testGitClient2 = Git.with(listener, new EnvVars()) + .in(testGitDir2) + .using((new Random()).nextBoolean() ? "git" : "jgit") + .getClient(); + } + + /* + * Regression test for a bug that accidentally resulted in empty build + * candidates. + * + * This test creates two repositories, one remote repository in testGitDir. + * A second repository is created in testGitDir2. + * + * We create the following commit graph with 3 commits and 3 tags in + * testGitDir: + * + * commit1(tag/a)---commit2(tag/b, tag/c)---commit3 + * + * We then clone this repository into testGitDir2 using the remote tags as + * refs with the given refspec. This is necessary to make the GitClient + * recognize the tags via getRemoteBranches. + * + * Candidates should only include the commit pointed to by tag/b or tag/c if + * the tags refspec is used and only tag/a has previously been built. If the + * refspec were expanded to include the master branch, then the candidate + * revisions would also include the master branch. + */ + @Test + public void testChooseWithMultipleTag() throws Exception { + commitNewFile("file-1-in-repo-1"); + ObjectId commit1 = testGitClient.revParse("HEAD"); + assertEquals(commit1, testGitClient.revParse("master")); + + testGitClient.tag("tag/a", "Applied tag/a to commit 1"); + + commitNewFile("file-2-in-repo-1"); + ObjectId commit2 = testGitClient.revParse("HEAD"); + assertEquals(commit2, testGitClient.revParse("master")); + + /* Two tags point to the same commit */ + testGitClient.tag("tag/b", "Applied tag/b to commit 2"); + testGitClient.tag("tag/c", "Applied tag/c to commit 2"); + assertEquals(commit2, testGitClient.revParse("tag/b")); + assertEquals(commit2, testGitClient.revParse("tag/c")); + + /* Advance master beyond the two tags */ + commitNewFile("file-3-in-repo-1"); + ObjectId commit3 = testGitClient.revParse("HEAD"); + assertEquals(commit3, testGitClient.revParse("master")); + + /* This refspec doesn't clone master branch, don't checkout master */ + RefSpec tagsRefSpec = new RefSpec("+refs/tags/tag/*:refs/remotes/origin/tags/tag/*"); + testGitClient2.clone_() + .refspecs(Arrays.asList(tagsRefSpec)) + .repositoryName("origin") + .url(testGitDir.getAbsolutePath()) + .execute(); + + /* Checkout either tag/b or tag/c, same results expected */ + String randomTag = (new Random()).nextBoolean() ? "tag/b" : "tag/c"; + testGitClient2.checkout().branch("my-branch").ref(randomTag).execute(); + assertEquals(commit2, testGitClient2.revParse("tag/b")); + assertEquals(commit2, testGitClient2.revParse("tag/c")); + + DefaultBuildChooser buildChooser = (DefaultBuildChooser) new GitSCM(testGitDir.getAbsolutePath()).getBuildChooser(); + + BuildData buildData = Mockito.mock(BuildData.class); + Mockito.when(buildData.hasBeenBuilt(testGitClient2.revParse("tag/a"))).thenReturn(true); + Mockito.when(buildData.hasBeenBuilt(testGitClient2.revParse("tag/b"))).thenReturn(false); + Mockito.when(buildData.hasBeenBuilt(testGitClient2.revParse("tag/c"))).thenReturn(false); + + BuildChooserContext context = Mockito.mock(BuildChooserContext.class); + Mockito.when(context.getEnvironment()).thenReturn(new EnvVars()); + + Collection candidateRevisions = buildChooser.getCandidateRevisions(false, "tag/*", testGitClient2, null, buildData, context); + assertEquals(1, candidateRevisions.size()); + String name = candidateRevisions.iterator().next().getBranches().iterator().next().getName(); + assertTrue("Expected .*/tags/b or .*/tags/c, was '" + name + "'", name.matches("(origin|refs)/tags/tag/[bc]")); + } + + /** + * Inline ${@link hudson.Functions#isWindows()} to prevent a transient + * remote classloader issue. + */ + private boolean isWindows() { + return File.pathSeparatorChar == ';'; + } +} diff --git a/src/test/java/hudson/plugins/git/util/CommitTimeComparatorTest.java b/src/test/java/hudson/plugins/git/util/CommitTimeComparatorTest.java index 2d03a499ec..153d42c8ea 100644 --- a/src/test/java/hudson/plugins/git/util/CommitTimeComparatorTest.java +++ b/src/test/java/hudson/plugins/git/util/CommitTimeComparatorTest.java @@ -1,6 +1,6 @@ package hudson.plugins.git.util; -import hudson.plugins.git.AbstractGitTestCase; +import hudson.plugins.git.AbstractGitRepository; import hudson.plugins.git.Branch; import hudson.plugins.git.Revision; @@ -9,15 +9,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author Kohsuke Kawaguchi */ -public class CommitTimeComparatorTest extends AbstractGitTestCase { - /** - * Verifies that the sort is old to new. - */ - public void testSort() throws Exception { +public class CommitTimeComparatorTest extends AbstractGitRepository { + + @Test + public void testSort_OrderIsOldToNew() throws Exception { boolean first = true; // create repository with three commits for (int i=0; i<3; i++) { @@ -25,13 +26,13 @@ public void testSort() throws Exception { if (first) first = false; else Thread.sleep(1000); - commit("file" + i, johnDoe, "Commit #" + i); - git.branch("branch" + i); + commitNewFile("file" + i); + testGitClient.branch("branch" + i); } - Map branches = new HashMap(); - List revs = new ArrayList(); - for (Branch b : git.getBranches()) { + Map branches = new HashMap<>(); + List revs = new ArrayList<>(); + for (Branch b : testGitClient.getBranches()) { if (!b.getName().startsWith("branch")) continue; Revision r = new Revision(b.getSHA1()); revs.add(r); @@ -42,7 +43,7 @@ public void testSort() throws Exception { for (int i=0; i<16; i++) { // shuffle, then sort. Collections.shuffle(revs); - Collections.sort(revs, new CommitTimeComparator(git.getRepository())); + Collections.sort(revs, new CommitTimeComparator(testGitClient.getRepository())); // it should be always branch1, branch2, branch3 for (int j=0; j<3; j++) diff --git a/src/test/java/hudson/plugins/git/util/DefaultBuildChooserTest.java b/src/test/java/hudson/plugins/git/util/DefaultBuildChooserTest.java index d8aa1c9232..baffe6321e 100644 --- a/src/test/java/hudson/plugins/git/util/DefaultBuildChooserTest.java +++ b/src/test/java/hudson/plugins/git/util/DefaultBuildChooserTest.java @@ -1,27 +1,46 @@ package hudson.plugins.git.util; +import hudson.plugins.git.AbstractGitRepository; import java.util.Collection; -import hudson.plugins.git.AbstractGitTestCase; import hudson.plugins.git.GitSCM; import hudson.plugins.git.Revision; +import static org.junit.Assert.*; +import org.junit.Test; /** * @author Arnout Engelen */ -public class DefaultBuildChooserTest extends AbstractGitTestCase { +public class DefaultBuildChooserTest extends AbstractGitRepository { + @Test public void testChooseGitRevisionToBuildByShaHash() throws Exception { - git.commit("Commit 1"); - String shaHashCommit1 = git.getBranches().iterator().next().getSHA1String(); - git.commit("Commit 2"); - String shaHashCommit2 = git.getBranches().iterator().next().getSHA1String(); + testGitClient.commit("Commit 1"); + String shaHashCommit1 = testGitClient.getBranches().iterator().next().getSHA1String(); + testGitClient.commit("Commit 2"); + String shaHashCommit2 = testGitClient.getBranches().iterator().next().getSHA1String(); assertNotSame(shaHashCommit1, shaHashCommit2); DefaultBuildChooser buildChooser = (DefaultBuildChooser) new GitSCM("foo").getBuildChooser(); - Collection candidateRevisions = buildChooser.getCandidateRevisions(false, shaHashCommit1, git, null, null, null); + Collection candidateRevisions = buildChooser.getCandidateRevisions(false, shaHashCommit1, testGitClient, null, null, null); assertEquals(1, candidateRevisions.size()); assertEquals(shaHashCommit1, candidateRevisions.iterator().next().getSha1String()); + + candidateRevisions = buildChooser.getCandidateRevisions(false, "aaa" + shaHashCommit1.substring(3), testGitClient, null, null, null); + assertTrue(candidateRevisions.isEmpty()); + } + + /* RegExp patterns prefixed with : should pass through to DefaultBuildChooser.getAdvancedCandidateRevisions */ + @Test + public void testIsAdvancedSpec() throws Exception { + DefaultBuildChooser buildChooser = (DefaultBuildChooser) new GitSCM("foo").getBuildChooser(); + + assertFalse(buildChooser.isAdvancedSpec("origin/master")); + assertTrue(buildChooser.isAdvancedSpec("origin/master-*")); + assertTrue(buildChooser.isAdvancedSpec("origin**")); + // regexp use case + assertTrue(buildChooser.isAdvancedSpec(":origin/master")); + assertTrue(buildChooser.isAdvancedSpec(":origin/master-\\d{*}")); } } diff --git a/src/test/java/hudson/plugins/git/util/GitUtilsJenkinsRuleTest.java b/src/test/java/hudson/plugins/git/util/GitUtilsJenkinsRuleTest.java new file mode 100644 index 0000000000..2f0e22108c --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/GitUtilsJenkinsRuleTest.java @@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright 2019 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git.util; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.Label; +import hudson.model.Node; +import hudson.model.TaskListener; +import hudson.model.labels.LabelAtom; +import hudson.plugins.git.GitTool; +import hudson.slaves.DumbSlave; +import hudson.util.StreamTaskListener; +import java.util.UUID; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class GitUtilsJenkinsRuleTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void testWorkspaceToNode() throws Exception { + String labelString = "label-" + UUID.randomUUID().toString(); + Label label = new LabelAtom(labelString); + DumbSlave agent = j.createOnlineSlave(label); + FilePath workspace = agent.getWorkspaceRoot(); + assertThat(GitUtils.workspaceToNode(workspace).getLabelString(), is(labelString)); + + /* Check that workspace on master reports master even when agent connected */ + assertThat(GitUtils.workspaceToNode(j.getInstance().getRootPath()), is(j.getInstance())); + } + + @Test + public void testWorkspaceToNodeRootPath() { + assertThat(GitUtils.workspaceToNode(j.getInstance().getRootPath()), is(j.getInstance())); + } + + @Test + public void testWorkspaceToNodeNullWorkspace() { + assertThat(GitUtils.workspaceToNode(null), is(j.getInstance())); + } + + @Test + public void testResolveGitTool() { + TaskListener listener = StreamTaskListener.NULL; + String gitTool = "Default"; + GitTool tool = GitUtils.resolveGitTool(gitTool, listener); + assertThat(tool.getGitExe(), startsWith("git")); + } + + @Test + public void testResolveGitToolNull() { + TaskListener listener = StreamTaskListener.NULL; + String gitTool = null; + GitTool tool = GitUtils.resolveGitTool(gitTool, listener); + assertThat(tool.getGitExe(), startsWith("git")); + } + + @Test + public void testResolveGitToolNonExistentTool() { + TaskListener listener = StreamTaskListener.NULL; + String gitTool = "non-existent-tool"; + GitTool tool = GitUtils.resolveGitTool(gitTool, listener); + assertThat(tool.getGitExe(), startsWith("git")); + } + + @Test + public void testResolveGitToolBuiltOnNull() { + TaskListener listener = StreamTaskListener.NULL; + String gitTool = null; + Node builtOn = null; + EnvVars env = new EnvVars(); + GitTool tool = GitUtils.resolveGitTool(gitTool, builtOn, env, listener); + assertThat(tool.getGitExe(), startsWith("git")); + } + + @Test + public void testResolveGitToolBuiltOnAgent() throws Exception { + TaskListener listener = StreamTaskListener.NULL; + String gitTool = "/opt/my-non-existing-git/bin/git"; + String labelString = "label-" + UUID.randomUUID().toString(); + Label label = new LabelAtom(labelString); + DumbSlave agent = j.createOnlineSlave(label); + EnvVars env = new EnvVars(); + GitTool tool = GitUtils.resolveGitTool(gitTool, agent, env, listener); + assertThat(tool.getGitExe(), startsWith("git")); + } +} diff --git a/src/test/java/hudson/plugins/git/util/GitUtilsTest.java b/src/test/java/hudson/plugins/git/util/GitUtilsTest.java new file mode 100644 index 0000000000..bbfb90ff8e --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/GitUtilsTest.java @@ -0,0 +1,363 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.git.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.Branch; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.Revision; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import jenkins.plugins.git.GitSampleRepoRule; +import org.eclipse.jgit.lib.ObjectId; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import static org.junit.Assert.assertThat; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class GitUtilsTest { + + @ClassRule + public static GitSampleRepoRule originRepo = new GitSampleRepoRule(); + + @ClassRule + public static TemporaryFolder repoParentFolder = new TemporaryFolder(); + + private static final String[] HEAD_BRANCH_NAMES = { + "master", + "sally-2", + "baker-1", + "able-4" + }; + private static final String OLDER_BRANCH_NAME = "older-branch"; + + private static ObjectId headId = null; + private static ObjectId headTag0Id = null; + private static ObjectId priorHeadId = null; + + private static Revision headRevision = null; + private static Revision headTag0Revision = null; + private static Revision priorRevision = null; + + private static final String PRIOR_TAG_NAME_1 = "prior-tag-1"; + private static final String PRIOR_TAG_NAME_2 = "prior-tag-2-annotated"; + private static final String HEAD_TAG_NAME_0 = "head-tag-0"; + private static final String HEAD_TAG_NAME_1 = "head-tag-1"; + private static final String HEAD_TAG_NAME_2 = "head-tag-2-annotated"; + private final String[] tagNames = { + PRIOR_TAG_NAME_1, + PRIOR_TAG_NAME_2, + HEAD_TAG_NAME_0, + HEAD_TAG_NAME_1, + HEAD_TAG_NAME_2 + }; + + private static List branchSpecList = null; + private static List priorBranchSpecList = null; + private static List branchList = null; + + private static final EnvVars ENV = new EnvVars(); + private static final TaskListener NULL_LISTENER = StreamTaskListener.NULL; + + private GitUtils gitUtils; + private static GitClient gitClient; + + private static final Random RANDOM = new Random(); + + @BeforeClass + public static void createSampleOriginRepo() throws Exception { + String fileName = "README"; + originRepo.init(); + originRepo.git("config", "user.name", "Author User Name"); + originRepo.git("config", "user.email", "author.user.name@mail.example.com"); + originRepo.git("tag", PRIOR_TAG_NAME_1); + originRepo.git("tag", "-a", PRIOR_TAG_NAME_2, "-m", "Annotated tag " + PRIOR_TAG_NAME_2); + priorHeadId = ObjectId.fromString(originRepo.head()); + + originRepo.git("checkout", "-b", OLDER_BRANCH_NAME); + branchList = new ArrayList<>(); + branchList.add(new Branch(OLDER_BRANCH_NAME, priorHeadId)); + branchList.add(new Branch("refs/tags/" + PRIOR_TAG_NAME_1, priorHeadId)); + branchList.add(new Branch("refs/tags/" + PRIOR_TAG_NAME_2, priorHeadId)); + priorRevision = new Revision(priorHeadId, branchList); + priorBranchSpecList = new ArrayList<>(); + priorBranchSpecList.add(new BranchSpec(OLDER_BRANCH_NAME)); + + originRepo.git("checkout", "master"); + originRepo.write(fileName, "This is the " + HEAD_TAG_NAME_0 + " README file " + RANDOM.nextInt()); + originRepo.git("add", fileName); + originRepo.git("commit", "-m", "Adding " + fileName + " tagged " + HEAD_TAG_NAME_0, fileName); + originRepo.git("tag", HEAD_TAG_NAME_0); + headTag0Id = ObjectId.fromString(originRepo.head()); + headTag0Revision = new Revision(headTag0Id); + + originRepo.write(fileName, "This is the README file " + RANDOM.nextInt()); + originRepo.git("add", fileName); + originRepo.git("commit", "-m", "Adding " + fileName, fileName); + originRepo.git("tag", HEAD_TAG_NAME_1); + originRepo.git("tag", "-a", HEAD_TAG_NAME_2, "-m", "Annotated tag " + HEAD_TAG_NAME_2); + headId = ObjectId.fromString(originRepo.head()); + branchSpecList = new ArrayList<>(); + branchList = new ArrayList<>(); + branchSpecList.add(new BranchSpec("master")); + branchSpecList.add(new BranchSpec("refs/tags/" + HEAD_TAG_NAME_0)); + branchSpecList.add(new BranchSpec("refs/tags/" + HEAD_TAG_NAME_1)); + branchSpecList.add(new BranchSpec("refs/tags/" + HEAD_TAG_NAME_2)); + branchList.add(new Branch("master", headId)); + branchList.add(new Branch("refs/tags/" + HEAD_TAG_NAME_0, headId)); + branchList.add(new Branch("refs/tags/" + HEAD_TAG_NAME_1, headId)); + branchList.add(new Branch("refs/tags/" + HEAD_TAG_NAME_2, headId)); + for (String branchName : HEAD_BRANCH_NAMES) { + if (!branchName.equals("master")) { + originRepo.git("checkout", "-b", branchName); + branchSpecList.add(new BranchSpec(branchName)); + branchList.add(new Branch(branchName, headId)); + } + } + originRepo.git("checkout", "master"); // Master branch as current branch in origin repo + headRevision = new Revision(headId, branchList); + + File gitDir = repoParentFolder.newFolder("test-repo"); + gitClient = Git.with(NULL_LISTENER, ENV).in(gitDir).using("git").getClient(); + gitClient.init(); + gitClient.clone_().url(originRepo.fileUrl()).repositoryName("origin").execute(); + gitClient.checkout("origin/master", "master"); + } + + @Before + public void createGitUtils() throws Exception { + gitUtils = new GitUtils(NULL_LISTENER, gitClient); + } + + @Test + public void testSortBranchesForRevision_Revision_List() { + Revision result = gitUtils.sortBranchesForRevision(headRevision, branchSpecList); + assertThat(result, is(headRevision)); + } + + @Test + public void testSortBranchesForRevision_Revision_List_Prior() { + Revision result = gitUtils.sortBranchesForRevision(priorRevision, priorBranchSpecList); + assertThat(result, is(priorRevision)); + } + + @Test + public void testSortBranchesForRevision_Revision_List_Mix_1() { + Revision result = gitUtils.sortBranchesForRevision(headRevision, priorBranchSpecList); + assertThat(result, is(headRevision)); + } + + @Test + public void testSortBranchesForRevision_Revision_List_Mix_2() { + Revision result = gitUtils.sortBranchesForRevision(priorRevision, branchSpecList); + assertThat(result, is(priorRevision)); + } + + @Test + public void testSortBranchesForRevision_Revision_List_Prior_3_args() { + Revision result = gitUtils.sortBranchesForRevision(headRevision, branchSpecList, ENV); + assertThat(result, is(headRevision)); + } + + @Test + public void testSortBranchesForRevision_3args() { + Revision result = gitUtils.sortBranchesForRevision(headRevision, branchSpecList, ENV); + assertThat(result, is(headRevision)); + } + + @Test + public void testSortBranchesForRevision_3args_Prior() { + Revision result = gitUtils.sortBranchesForRevision(priorRevision, branchSpecList, ENV); + assertThat(result, is(priorRevision)); + } + + @Test + public void testGetRevisionContainingBranch() throws Exception { + for (String branchName : HEAD_BRANCH_NAMES) { + Revision revision = gitUtils.getRevisionContainingBranch("origin/" + branchName); + assertThat(revision, is(headRevision)); + } + } + + @Test + public void testGetRevisionContainingBranch_OlderName() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("origin/" + OLDER_BRANCH_NAME); + assertThat(revision, is(priorRevision)); + } + + /* Tags are searched in getRevisionContainingBranch beginning with 3.2.0 */ + @Test + public void testGetRevisionContainingBranch_UseTagNameHead0() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("refs/tags/" + HEAD_TAG_NAME_0); + assertThat(revision, is(headTag0Revision)); + } + + /* Tags are searched in getRevisionContainingBranch beginning with 3.2.0 */ + @Test + public void testGetRevisionContainingBranch_UseTagNameHead1() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("refs/tags/" + HEAD_TAG_NAME_1); + assertThat(revision, is(headRevision)); + } + + /* Tags are searched in getRevisionContainingBranch beginning with 3.2.0 */ + @Test + public void testGetRevisionContainingBranch_UseTagNameHead2() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("refs/tags/" + HEAD_TAG_NAME_2); + assertThat(revision, is(headRevision)); + } + + /* Tags are searched in getRevisionContainingBranch beginning with 3.2.0 */ + @Test + public void testGetRevisionContainingBranch_UseTagNamePrior1() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("refs/tags/" + PRIOR_TAG_NAME_1); + assertThat(revision, is(priorRevision)); + } + + /* Tags are searched in getRevisionContainingBranch beginning with 3.2.0 */ + @Test + public void testGetRevisionContainingBranch_UseTagNamePrior2() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("refs/tags/" + PRIOR_TAG_NAME_2); + assertThat(revision, is(priorRevision)); + } + + @Test + public void testGetRevisionContainingBranch_InvalidBranchName() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("origin/not-a-valid-branch-name"); + assertThat(revision, is(nullValue(Revision.class))); + } + + @Test + public void testGetRevisionContainingBranch_InvalidTagName() throws Exception { + Revision revision = gitUtils.getRevisionContainingBranch("ref/tags/not-a-valid-tag-name"); + assertThat(revision, is(nullValue(Revision.class))); + } + + @Test + public void testGetRevisionForSHA1() throws Exception { + Revision revision = gitUtils.getRevisionForSHA1(headId); + assertThat(revision, is(headRevision)); + } + + @Test + public void testGetRevisionForSHA1PriorRevision() throws Exception { + Revision revision = gitUtils.getRevisionForSHA1(priorHeadId); + assertThat(revision, is(priorRevision)); + } + + @Test + public void testGetRevisionForSHA1UnknownRevision() throws Exception { + ObjectId unknown = ObjectId.fromString("a422d10c6dc4262effb12f9e7a64911111000000"); + Revision unknownRevision = new Revision(unknown); + Revision revision = gitUtils.getRevisionForSHA1(unknown); + assertThat(revision, is(unknownRevision)); + } + + @Test + public void testFilterTipBranches() throws Exception { + Collection multiRevisionList = new ArrayList<>(); + multiRevisionList.add(priorRevision); + multiRevisionList.add(headRevision); + Collection filteredRevisions = new ArrayList<>(); + filteredRevisions.add(headRevision); + List result = gitUtils.filterTipBranches(multiRevisionList); + assertThat(result, is(filteredRevisions)); + } + + @Test + public void testFilterTipBranchesNoRemovals() throws Exception { + Collection headRevisionList = new ArrayList<>(); + headRevisionList.add(headRevision); + List result = gitUtils.filterTipBranches(headRevisionList); + assertThat(result, is(headRevisionList)); + } + + @Test + public void testFilterTipBranchesNoRemovalsNonTip() throws Exception { + Collection priorRevisionList = new ArrayList<>(); + priorRevisionList.add(priorRevision); + List result = gitUtils.filterTipBranches(priorRevisionList); + assertThat(result, is(priorRevisionList)); + } + + @Test + public void testFixupNames() { + String[] names = {"origin", "origin2", null, "", null}; + String[] urls = { + "git://github.com/jenkinsci/git-plugin.git", + "git@github.com:jenkinsci/git-plugin.git", + "https://github.com/jenkinsci/git-plugin", + "https://github.com/jenkinsci/git-plugin.git", + "ssh://github.com/jenkinsci/git-plugin.git" + }; + String[] expected = {"origin", "origin2", "origin1", "origin3", "origin4"}; + String[] actual = GitUtils.fixupNames(names, urls); + assertThat(expected, is(actual)); + } + + private Set getExpectedNames() { + Set names = new HashSet<>(HEAD_BRANCH_NAMES.length + tagNames.length + 1); + for (String branchName : HEAD_BRANCH_NAMES) { + names.add("origin/" + branchName); + } + names.add("origin/" + OLDER_BRANCH_NAME); + for (String tagName : tagNames) { + names.add("refs/tags/" + tagName); + } + return names; + } + + private Set getActualNames(@NonNull Collection revisions) { + Set names = new HashSet<>(revisions.size()); + for (Revision revision : revisions) { + for (Branch branch : revision.getBranches()) { + names.add(branch.getName()); + } + } + return names; + } + + @Test + public void testGetAllBranchRevisions() throws Exception { + Collection allRevisions = gitUtils.getAllBranchRevisions(); + assertThat(allRevisions, hasItem(headRevision)); + Set expectedNames = getExpectedNames(); + Set actualNames = getActualNames(allRevisions); + assertThat(actualNames, is(expectedNames)); + } +} diff --git a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java new file mode 100644 index 0000000000..e637d6030d --- /dev/null +++ b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java @@ -0,0 +1,1117 @@ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Actionable; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.extensions.impl.IgnoreNotifyCommit; +import hudson.scm.SCMRevisionState; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.plugins.git.traits.DiscoverOtherRefsTrait; +import jenkins.plugins.git.traits.IgnoreOnPushNotificationTrait; +import jenkins.plugins.git.traits.PruneStaleBranchTrait; +import jenkins.plugins.git.traits.TagDiscoveryTrait; + +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; + +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; + +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.TestJGitAPIImpl; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; + +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.collection.IsMapContaining.hasKey; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.CombinableMatcher.both; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; +import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo; +import static org.hamcrest.number.OrderingComparison.lessThanOrEqualTo; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link AbstractGitSCMSource} + */ +public class AbstractGitSCMSourceTest { + + static final String GitBranchSCMHead_DEV_MASTER = "[GitBranchSCMHead{name='dev', ref='refs/heads/dev'}, GitBranchSCMHead{name='master', ref='refs/heads/master'}]"; + static final String GitBranchSCMHead_DEV_DEV2_MASTER = "[GitBranchSCMHead{name='dev', ref='refs/heads/dev'}, GitBranchSCMHead{name='dev2', ref='refs/heads/dev2'}, GitBranchSCMHead{name='master', ref='refs/heads/master'}]"; + + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + @Rule + public GitSampleRepoRule sampleRepo2 = new GitSampleRepoRule(); + + // TODO AbstractGitSCMSourceRetrieveHeadsTest *sounds* like it would be the right place, but it does not in fact retrieve any heads! + @Issue("JENKINS-37482") + @Test + public void retrieveHeads() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + // And reuse cache: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + sampleRepo.git("checkout", "-b", "dev2"); + sampleRepo.write("file", "modified again"); + sampleRepo.git("commit", "--all", "--message=dev2"); + // After changing data: + assertEquals(GitBranchSCMHead_DEV_DEV2_MASTER, source.fetch(listener).toString()); + } + + @Test + public void retrieveHeadsRequiresBranchDiscovery() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals("[]", source.fetch(listener).toString()); + source.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + // And reuse cache: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + sampleRepo.git("checkout", "-b", "dev2"); + sampleRepo.write("file", "modified again"); + sampleRepo.git("commit", "--all", "--message=dev2"); + // After changing data: + assertEquals(GitBranchSCMHead_DEV_DEV2_MASTER, source.fetch(listener).toString()); + } + + @Issue("JENKINS-46207") + @Test + public void retrieveHeadsSupportsTagDiscovery_ignoreTagsWithoutTagDiscoveryTrait() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + sampleRepo.git("tag", "lightweight"); + sampleRepo.write("file", "modified2"); + sampleRepo.git("commit", "--all", "--message=dev2"); + sampleRepo.git("tag", "-a", "annotated", "-m", "annotated"); + sampleRepo.write("file", "modified3"); + sampleRepo.git("commit", "--all", "--message=dev3"); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals("[]", source.fetch(listener).toString()); + source.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + // And reuse cache: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + sampleRepo.git("checkout", "-b", "dev2"); + sampleRepo.write("file", "modified again"); + sampleRepo.git("commit", "--all", "--message=dev2"); + // After changing data: + assertEquals(GitBranchSCMHead_DEV_DEV2_MASTER, source.fetch(listener).toString()); + } + + @Issue("JENKINS-46207") + @Test + public void retrieveHeadsSupportsTagDiscovery_findTagsWithTagDiscoveryTrait() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev-commit-message"); + long beforeLightweightTag = System.currentTimeMillis(); + sampleRepo.git("tag", "lightweight"); + long afterLightweightTag = System.currentTimeMillis(); + sampleRepo.write("file", "modified2"); + sampleRepo.git("commit", "--all", "--message=dev2-commit-message"); + long beforeAnnotatedTag = System.currentTimeMillis(); + sampleRepo.git("tag", "-a", "annotated", "-m", "annotated"); + long afterAnnotatedTag = System.currentTimeMillis(); + sampleRepo.write("file", "modified3"); + sampleRepo.git("commit", "--all", "--message=dev3-commit-message"); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(new ArrayList<>()); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals("[]", source.fetch(listener).toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + Set scmHeadSet = source.fetch(listener); + long now = System.currentTimeMillis(); + for (SCMHead scmHead : scmHeadSet) { + if (scmHead instanceof GitTagSCMHead) { + GitTagSCMHead tagHead = (GitTagSCMHead) scmHead; + // FAT file system time stamps only resolve to 2 second boundary + // EXT3 file system time stamps only resolve to 1 second boundary + long fileTimeStampFuzz = isWindows() ? 2000L : 1000L; + switch (scmHead.getName()) { + case "lightweight": + { + long timeStampDelta = afterLightweightTag - tagHead.getTimestamp(); + assertThat(timeStampDelta, is(both(greaterThanOrEqualTo(0L)).and(lessThanOrEqualTo(afterLightweightTag - beforeLightweightTag + fileTimeStampFuzz)))); + break; + } + case "annotated": + { + long timeStampDelta = afterAnnotatedTag - tagHead.getTimestamp(); + assertThat(timeStampDelta, is(both(greaterThanOrEqualTo(0L)).and(lessThanOrEqualTo(afterAnnotatedTag - beforeAnnotatedTag + fileTimeStampFuzz)))); + break; + } + default: + fail("Unexpected tag head '" + scmHead.getName() + "'"); + break; + } + } + } + String expected = "[SCMHead{'annotated'}, GitBranchSCMHead{name='dev', ref='refs/heads/dev'}, SCMHead{'lightweight'}, GitBranchSCMHead{name='master', ref='refs/heads/master'}]"; + assertEquals(expected, scmHeadSet.toString()); + // And reuse cache: + assertEquals(expected, source.fetch(listener).toString()); + sampleRepo.git("checkout", "-b", "dev2"); + sampleRepo.write("file", "modified again"); + sampleRepo.git("commit", "--all", "--message=dev2"); + // After changing data: + expected = "[SCMHead{'annotated'}, GitBranchSCMHead{name='dev', ref='refs/heads/dev'}, GitBranchSCMHead{name='dev2', ref='refs/heads/dev2'}, SCMHead{'lightweight'}, GitBranchSCMHead{name='master', ref='refs/heads/master'}]"; + assertEquals(expected, source.fetch(listener).toString()); + } + + @Issue("JENKINS-46207") + @Test + public void retrieveHeadsSupportsTagDiscovery_onlyTagsWithoutBranchDiscoveryTrait() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + sampleRepo.git("tag", "lightweight"); + sampleRepo.write("file", "modified2"); + sampleRepo.git("commit", "--all", "--message=dev2"); + sampleRepo.git("tag", "-a", "annotated", "-m", "annotated"); + sampleRepo.write("file", "modified3"); + sampleRepo.git("commit", "--all", "--message=dev3"); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(new ArrayList<>()); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals("[]", source.fetch(listener).toString()); + source.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + assertEquals("[SCMHead{'annotated'}, SCMHead{'lightweight'}]", source.fetch(listener).toString()); + // And reuse cache: + assertEquals("[SCMHead{'annotated'}, SCMHead{'lightweight'}]", source.fetch(listener).toString()); + } + + @Issue("JENKINS-45953") + @Test + public void retrieveRevisions() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + sampleRepo.git("tag", "lightweight"); + sampleRepo.write("file", "modified2"); + sampleRepo.git("commit", "--all", "--message=dev2"); + sampleRepo.git("tag", "-a", "annotated", "-m", "annotated"); + sampleRepo.write("file", "modified3"); + sampleRepo.git("commit", "--all", "--message=dev3"); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(new ArrayList<>()); + TaskListener listener = StreamTaskListener.fromStderr(); + assertThat(source.fetchRevisions(listener, null), hasSize(0)); + source.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + assertThat(source.fetchRevisions(listener, null), containsInAnyOrder("dev", "master")); + source.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + assertThat(source.fetchRevisions(listener, null), containsInAnyOrder("annotated", "lightweight")); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + assertThat(source.fetchRevisions(listener, null), containsInAnyOrder("dev", "master", "annotated", "lightweight")); + } + + @Issue("JENKINS-47824") + @Test + public void retrieveByName() throws Exception { + sampleRepo.init(); + String masterHash = sampleRepo.head(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + sampleRepo.git("tag", "v1"); + String v1Hash = sampleRepo.head(); + sampleRepo.write("file", "modified2"); + sampleRepo.git("commit", "--all", "--message=dev2"); + sampleRepo.git("tag", "-a", "v2", "-m", "annotated"); + String v2Hash = sampleRepo.head(); + sampleRepo.write("file", "modified3"); + sampleRepo.git("commit", "--all", "--message=dev3"); + String devHash = sampleRepo.head(); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(new ArrayList<>()); + + TaskListener listener = StreamTaskListener.fromStderr(); + + listener.getLogger().println("\n=== fetch('master') ===\n"); + SCMRevision rev = source.fetch("master", listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl)rev).getHash(), is(masterHash)); + listener.getLogger().println("\n=== fetch('dev') ===\n"); + rev = source.fetch("dev", listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl)rev).getHash(), is(devHash)); + listener.getLogger().println("\n=== fetch('v1') ===\n"); + rev = source.fetch("v1", listener, null); + assertThat(rev, instanceOf(GitTagSCMRevision.class)); + assertThat(((GitTagSCMRevision)rev).getHash(), is(v1Hash)); + listener.getLogger().println("\n=== fetch('v2') ===\n"); + rev = source.fetch("v2", listener, null); + assertThat(rev, instanceOf(GitTagSCMRevision.class)); + assertThat(((GitTagSCMRevision)rev).getHash(), is(v2Hash)); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", masterHash); + rev = source.fetch(masterHash, listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(masterHash)); + assertThat(rev.getHead().getName(), is("master")); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", masterHash.substring(0, 10)); + rev = source.fetch(masterHash.substring(0, 10), listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(masterHash)); + assertThat(rev.getHead().getName(), is("master")); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", devHash); + rev = source.fetch(devHash, listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(devHash)); + assertThat(rev.getHead().getName(), is("dev")); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", devHash.substring(0, 10)); + rev = source.fetch(devHash.substring(0, 10), listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(devHash)); + assertThat(rev.getHead().getName(), is("dev")); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", v1Hash); + rev = source.fetch(v1Hash, listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(v1Hash)); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", v1Hash.substring(0, 10)); + rev = source.fetch(v1Hash.substring(0, 10), listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(v1Hash)); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", v2Hash); + rev = source.fetch(v2Hash, listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(v2Hash)); + + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", v2Hash.substring(0, 10)); + rev = source.fetch(v2Hash.substring(0, 10), listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(v2Hash)); + + String v2Tag = "refs/tags/v2"; + listener.getLogger().printf("%n=== fetch('%s') ===%n%n", v2Tag); + rev = source.fetch(v2Tag, listener, null); + assertThat(rev, instanceOf(AbstractGitSCMSource.SCMRevisionImpl.class)); + assertThat(((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(), is(v2Hash)); + + } + + public static abstract class ActionableSCMSourceOwner extends Actionable implements SCMSourceOwner { + + } + + @Test + public void retrievePrimaryHead_NotDuplicated() throws Exception { + retrievePrimaryHead(false); + } + + @Test + public void retrievePrimaryHead_Duplicated() throws Exception { + retrievePrimaryHead(true); + } + + public void retrievePrimaryHead(boolean duplicatePrimary) throws Exception { + sampleRepo.init(); + sampleRepo.write("file.txt", ""); + sampleRepo.git("add", "file.txt"); + sampleRepo.git("commit", "--all", "--message=add-empty-file"); + sampleRepo.git("checkout", "-b", "new-primary"); + sampleRepo.write("file.txt", "content"); + sampleRepo.git("add", "file.txt"); + sampleRepo.git("commit", "--all", "--message=add-file"); + if (duplicatePrimary) { + // If more than one branch points to same sha1 as new-primary and the + // command line git implementation is older than 2.8.0, then the guesser + // for primary won't be able to choose between the two alternatives. + // The next line illustrates that case with older command line git. + sampleRepo.git("checkout", "-b", "new-primary-duplicate", "new-primary"); + } + sampleRepo.git("checkout", "master"); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.git("symbolic-ref", "HEAD", "refs/heads/new-primary"); + + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + ActionableSCMSourceOwner owner = Mockito.mock(ActionableSCMSourceOwner.class); + when(owner.getSCMSource(source.getId())).thenReturn(source); + when(owner.getSCMSources()).thenReturn(Collections.singletonList(source)); + source.setOwner(owner); + TaskListener listener = StreamTaskListener.fromStderr(); + Map headByName = new TreeMap<>(); + for (SCMHead h: source.fetch(listener)) { + headByName.put(h.getName(), h); + } + if (duplicatePrimary) { + assertThat(headByName.keySet(), containsInAnyOrder("master", "dev", "new-primary", "new-primary-duplicate")); + } else { + assertThat(headByName.keySet(), containsInAnyOrder("master", "dev", "new-primary")); + } + List actions = source.fetchActions(null, listener); + GitRemoteHeadRefAction refAction = null; + for (Action a: actions) { + if (a instanceof GitRemoteHeadRefAction) { + refAction = (GitRemoteHeadRefAction) a; + break; + } + } + final boolean CLI_GIT_LESS_THAN_280 = !sampleRepo.gitVersionAtLeast(2, 8); + if (duplicatePrimary && CLI_GIT_LESS_THAN_280) { + assertThat(refAction, is(nullValue())); + } else { + assertThat(refAction, notNullValue()); + assertThat(refAction.getName(), is("new-primary")); + when(owner.getAction(GitRemoteHeadRefAction.class)).thenReturn(refAction); + when(owner.getActions(GitRemoteHeadRefAction.class)).thenReturn(Collections.singletonList(refAction)); + actions = source.fetchActions(headByName.get("new-primary"), null, listener); + } + + PrimaryInstanceMetadataAction primary = null; + for (Action a: actions) { + if (a instanceof PrimaryInstanceMetadataAction) { + primary = (PrimaryInstanceMetadataAction) a; + break; + } + } + if (duplicatePrimary && CLI_GIT_LESS_THAN_280) { + assertThat(primary, is(nullValue())); + } else { + assertThat(primary, notNullValue()); + } + } + + @Issue("JENKINS-31155") + @Test + public void retrieveRevision() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of branches: + assertEquals("v2", fileAt("master", run, source, listener)); + assertEquals("v3", fileAt("dev", run, source, listener)); + // Tags: + assertEquals("v1", fileAt("v1", run, source, listener)); + // And commit hashes: + assertEquals("v1", fileAt(v1, run, source, listener)); + assertEquals("v1", fileAt(v1.substring(0, 7), run, source, listener)); + // Nonexistent stuff: + assertNull(fileAt("nonexistent", run, source, listener)); + assertNull(fileAt("1234567", run, source, listener)); + assertNull(fileAt("", run, source, listener)); + assertNull(fileAt("\n", run, source, listener)); + assertThat(source.fetchRevisions(listener, null), hasItems("master", "dev", "v1")); + // we do not care to return commit hashes or other references + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_nonHead() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt(v3, run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + @Ignore("At least file:// protocol doesn't allow fetching unannounced commits") + public void retrieveRevision_nonAdvertised() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("reset", "--hard", "HEAD^"); // dev, the v3 ref is eligible for GC but still fetchable + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt(v3, run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_customRef() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/custom/foo", v3); // now this is an advertised ref so cannot be GC'd + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(), + new TagDiscoveryTrait(), + new DiscoverOtherRefsTrait("refs/custom/foo"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt(v3, run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_customRef_descendant() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + String v2 = sampleRepo.head(); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + sampleRepo.git("update-ref", "refs/custom/foo", v3); // now this is an advertised ref so cannot be GC'd + sampleRepo.git("reset", "--hard", "HEAD~2"); // dev + String dev = sampleRepo.head(); + assertNotEquals(dev, v3); //Just verifying the reset nav got correct + assertEquals(dev, v2); + sampleRepo.write("file", "v5"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(), + new TagDiscoveryTrait(), + new DiscoverOtherRefsTrait("refs/custom/*"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt(v3, run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_customRef_abbrev_sha1() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/custom/foo", v3); // now this is an advertised ref so cannot be GC'd + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(), + new TagDiscoveryTrait(), + new DiscoverOtherRefsTrait("refs/custom/foo"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt(v3.substring(0, 7), run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_pr_refspec() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/pull-requests/1/from", v3); // now this is an advertised ref so cannot be GC'd + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait(), new DiscoverOtherRefsTrait("pull-requests/*/from"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt("pull-requests/1/from", run, source, listener)); + } + + @Issue("JENKINS-48061") + @Test + public void retrieveRevision_pr_local_refspec() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/pull-requests/1/from", v3); // now this is an advertised ref so cannot be GC'd + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + //new RefSpecsSCMSourceTrait("+refs/pull-requests/*/from:refs/remotes/@{remote}/pr/*") + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait(), + new DiscoverOtherRefsTrait("/pull-requests/*/from", "pr/@{1}"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + // Test retrieval of non head revision: + assertEquals("v3", fileAt("pr/1", run, source, listener)); + } + + private int wsCount; + private String fileAt(String revision, Run run, SCMSource source, TaskListener listener) throws Exception { + SCMRevision rev = source.fetch(revision, listener, null); + if (rev == null) { + return null; + } else { + FilePath ws = new FilePath(run.getRootDir()).child("ws" + ++wsCount); + source.build(rev.getHead(), rev).checkout(run, new Launcher.LocalLauncher(listener), ws, listener, null, SCMRevisionState.NONE); + return ws.child("file").readToString(); + } + } + + @Issue("JENKINS-48061") + @Test + public void fetchOtherRef() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/custom/1", v3); + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait(), new DiscoverOtherRefsTrait("custom/*"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + + final SCMHeadObserver.Collector collector = + source.fetch(new SCMSourceCriteria() { + @Override + public boolean isHead(@NonNull Probe probe, @NonNull TaskListener listener) throws IOException { + return true; + } + }, new SCMHeadObserver.Collector(), listener); + + final Map result = collector.result(); + assertThat(result.entrySet(), hasSize(4)); + assertThat(result, hasKey(allOf( + instanceOf(GitRefSCMHead.class), + hasProperty("name", equalTo("custom-1")) + ))); + } + + @Issue("JENKINS-48061") + @Test + public void fetchOtherRevisions() throws Exception { + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=v1"); + sampleRepo.git("tag", "v1"); + String v1 = sampleRepo.head(); + sampleRepo.write("file", "v2"); + sampleRepo.git("commit", "--all", "--message=v2"); // master + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "v3"); + sampleRepo.git("commit", "--all", "--message=v3"); // dev + String v3 = sampleRepo.head(); + sampleRepo.git("update-ref", "refs/custom/1", v3); + sampleRepo.git("reset", "--hard", "HEAD^"); // dev + sampleRepo.write("file", "v4"); + sampleRepo.git("commit", "--all", "--message=v4"); // dev + // SCM.checkout does not permit a null build argument, unfortunately. + Run run = r.buildAndAssertSuccess(r.createFreeStyleProject()); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait(), new DiscoverOtherRefsTrait("custom/*"))); + StreamTaskListener listener = StreamTaskListener.fromStderr(); + + final Set revisions = source.fetchRevisions(listener, null); + + assertThat(revisions, hasSize(4)); + assertThat(revisions, containsInAnyOrder( + equalTo("custom-1"), + equalTo("v1"), + equalTo("dev"), + equalTo("master") + )); + } + + @Issue("JENKINS-37727") + @Test + public void pruneRemovesDeletedBranches() throws Exception { + sampleRepo.init(); + + /* Write a file to the master branch */ + sampleRepo.write("master-file", "master-content-" + UUID.randomUUID().toString()); + sampleRepo.git("add", "master-file"); + sampleRepo.git("commit", "--message=master-branch-commit-message"); + + /* Write a file to the dev branch */ + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("dev-file", "dev-content-" + UUID.randomUUID().toString()); + sampleRepo.git("add", "dev-file"); + sampleRepo.git("commit", "--message=dev-branch-commit-message"); + + /* Fetch from sampleRepo */ + GitSCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + TaskListener listener = StreamTaskListener.fromStderr(); + // SCMHeadObserver.Collector.result is a TreeMap so order is predictable: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + // And reuse cache: + assertEquals(GitBranchSCMHead_DEV_MASTER, source.fetch(listener).toString()); + + /* Create dev2 branch and write a file to it */ + sampleRepo.git("checkout", "-b", "dev2", "master"); + sampleRepo.write("dev2-file", "dev2-content-" + UUID.randomUUID().toString()); + sampleRepo.git("add", "dev2-file"); + sampleRepo.git("commit", "--message=dev2-branch-commit-message"); + + // Verify new branch is visible + assertEquals(GitBranchSCMHead_DEV_DEV2_MASTER, source.fetch(listener).toString()); + + /* Delete the dev branch */ + sampleRepo.git("branch", "-D", "dev"); + + /* Fetch and confirm dev branch was pruned */ + assertEquals("[GitBranchSCMHead{name='dev2', ref='refs/heads/dev2'}, GitBranchSCMHead{name='master', ref='refs/heads/master'}]", source.fetch(listener).toString()); + } + + @Test + public void testSpecificRevisionBuildChooser() throws Exception { + sampleRepo.init(); + + /* Write a file to the master branch */ + sampleRepo.write("master-file", "master-content-" + UUID.randomUUID().toString()); + sampleRepo.git("add", "master-file"); + sampleRepo.git("commit", "--message=master-branch-commit-message"); + + /* Fetch from sampleRepo */ + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Collections.singletonList(new IgnoreOnPushNotificationTrait())); + List extensions = new ArrayList<>(); + assertThat(source.getExtensions(), is(empty())); + LocalBranch localBranchExtension = new LocalBranch("**"); + extensions.add(localBranchExtension); + source.setExtensions(extensions); + assertThat(source.getExtensions(), contains( + allOf( + instanceOf(LocalBranch.class), + hasProperty("localBranch", is("**") + ) + ) + )); + + SCMHead head = new SCMHead("master"); + SCMRevision revision = new AbstractGitSCMSource.SCMRevisionImpl(head, "beaded4deed2bed4feed2deaf78933d0f97a5a34"); + + // because we are ignoring push notifications we also ignore commits + extensions.add(new IgnoreNotifyCommit()); + + /* Check that BuildChooserSetting not added to extensions by build() */ + GitSCM scm = (GitSCM) source.build(head); + assertThat(scm.getExtensions(), containsInAnyOrder( + allOf( + instanceOf(LocalBranch.class), + hasProperty("localBranch", is("**") + ) + ), + // no BuildChooserSetting + instanceOf(IgnoreNotifyCommit.class), + instanceOf(GitSCMSourceDefaults.class) + )); + + /* Check that BuildChooserSetting has been added to extensions by build() */ + GitSCM scmRevision = (GitSCM) source.build(head, revision); + assertThat(scmRevision.getExtensions(), containsInAnyOrder( + allOf( + instanceOf(LocalBranch.class), + hasProperty("localBranch", is("**") + ) + ), + instanceOf(BuildChooserSetting.class), + instanceOf(IgnoreNotifyCommit.class), + instanceOf(GitSCMSourceDefaults.class) + )); + } + + + @Test + public void testCustomRemoteName() throws Exception { + sampleRepo.init(); + + GitSCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "upstream", null, "*", "", true); + SCMHead head = new SCMHead("master"); + GitSCM scm = (GitSCM) source.build(head); + List configs = scm.getUserRemoteConfigs(); + assertEquals(1, configs.size()); + UserRemoteConfig config = configs.get(0); + assertEquals("upstream", config.getName()); + assertEquals("+refs/heads/*:refs/remotes/upstream/*", config.getRefspec()); + } + + @Test + public void testCustomRefSpecs() throws Exception { + sampleRepo.init(); + + GitSCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", null, "+refs/heads/*:refs/remotes/origin/* +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*", "*", "", true); + SCMHead head = new SCMHead("master"); + GitSCM scm = (GitSCM) source.build(head); + List configs = scm.getUserRemoteConfigs(); + + assertEquals(1, configs.size()); + + UserRemoteConfig config = configs.get(0); + assertEquals("origin", config.getName()); + assertEquals("+refs/heads/*:refs/remotes/origin/* +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*", config.getRefspec()); + } + + @Test + public void refLockEncounteredIfPruneTraitNotPresentOnNotFoundRetrieval() throws Exception { + TaskListener listener = StreamTaskListener.fromStderr(); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits((Collections.singletonList(new BranchDiscoveryTrait()))); + + createRefLockEnvironment(listener, source); + + try { + source.fetch("v1.2", listener, null); + } catch (GitException e){ + assertFalse(e.getMessage().contains("--prune")); + return; + } + //fail if ref lock does not occur + fail(); + } + + @Test + public void refLockEncounteredIfPruneTraitNotPresentOnTagRetrieval() throws Exception { + TaskListener listener = StreamTaskListener.fromStderr(); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits((Collections.singletonList(new TagDiscoveryTrait()))); + + createRefLockEnvironment(listener, source); + + try { + source.fetch("v1.2", listener, null); + } catch (GitException e){ + assertFalse(e.getMessage().contains("--prune")); + return; + } + //fail if ref lock does not occur + fail(); + } + + @Test + public void refLockAvoidedIfPruneTraitPresentOnNotFoundRetrieval() throws Exception { + /* Older git versions have unexpected behaviors with prune */ + assumeTrue(sampleRepo.gitVersionAtLeast(1, 9, 0)); + TaskListener listener = StreamTaskListener.fromStderr(); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits((Arrays.asList(new TagDiscoveryTrait(), new PruneStaleBranchTrait()))); + + createRefLockEnvironment(listener, source); + + source.fetch("v1.2", listener, null); + + assertEquals("[SCMHead{'v1.2'}]", source.fetch(listener).toString()); + } + + @Test + public void refLockAvoidedIfPruneTraitPresentOnTagRetrieval() throws Exception { + /* Older git versions have unexpected behaviors with prune */ + assumeTrue(sampleRepo.gitVersionAtLeast(1, 9, 0)); + TaskListener listener = StreamTaskListener.fromStderr(); + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits((Arrays.asList(new TagDiscoveryTrait(), new PruneStaleBranchTrait()))); + + createRefLockEnvironment(listener, source); + + source.fetch("v1.2", listener, null); + + assertEquals("[SCMHead{'v1.2'}]", source.fetch(listener).toString()); + } + + private void createRefLockEnvironment(TaskListener listener, GitSCMSource source) throws Exception { + String branch = "prune"; + String branchRefLock = "prune/prune"; + sampleRepo.init(); + + //Create branch x + sampleRepo.git("checkout", "-b", branch); + sampleRepo.git("push", "--set-upstream", source.getRemote(), branch); + + //Ensure source retrieval has fetched branch x + source.fetch("v1.2", listener, null); + + //Remove branch x + sampleRepo.git("checkout", "master"); + sampleRepo.git("push", source.getRemote(), "--delete", branch); + + //Create branch x/x (ref lock engaged) + sampleRepo.git("checkout", "-b", branchRefLock); + sampleRepo.git("push", "--set-upstream", source.getRemote(), branchRefLock); + + //create tag for retrieval + sampleRepo.git("tag", "v1.2"); + sampleRepo.git("push", source.getRemote(), "v1.2"); + } + + @Test @Issue("JENKINS-50394") + public void when_commits_added_during_discovery_we_do_not_crash() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + System.setProperty(Git.class.getName() + ".mockClient", MockGitClient.class.getName()); + sharedSampleRepo = sampleRepo; + try { + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + source.setTraits(Arrays.asList(new BranchDiscoveryTrait())); + TaskListener listener = StreamTaskListener.fromStderr(); + SCMHeadObserver.Collector c = source.fetch(new SCMSourceCriteria() { + @Override + public boolean isHead(@NonNull Probe probe, @NonNull TaskListener listener) throws IOException { + return true; + } + }, new SCMHeadObserver.Collector(), listener); + + assertThat(c.result().keySet(), containsInAnyOrder( + hasProperty("name", equalTo("master")), + hasProperty("name", equalTo("dev")) + )); + } catch(MissingObjectException me) { + fail("Not supposed to get MissingObjectException"); + } finally { + System.clearProperty(Git.class.getName() + ".mockClient"); + sharedSampleRepo = null; + } + } + //Ugly but MockGitClient needs to be static and no good way to pass it on + static GitSampleRepoRule sharedSampleRepo; + + public static class MockGitClient extends TestJGitAPIImpl { + final String exe; + final EnvVars env; + + public MockGitClient(String exe, EnvVars env, File workspace, TaskListener listener) { + super(workspace, listener); + this.exe = exe; + this.env = env; + } + + @Override + public Map getRemoteReferences(String url, String pattern, boolean headsOnly, boolean tagsOnly) throws GitException, InterruptedException { + final Map remoteReferences = super.getRemoteReferences(url, pattern, headsOnly, tagsOnly); + try { + //Now update the repo with new commits + sharedSampleRepo.write("file2", "New"); + sharedSampleRepo.git("add", "file2"); + sharedSampleRepo.git("commit", "--all", "--message=inbetween"); + } catch (Exception e) { + throw new GitException("Sneaking in something didn't work", e); + } + return remoteReferences; + } + + @Override + public FetchCommand fetch_() { + final FetchCommand fetchCommand = super.fetch_(); + //returning something that updates the repo after the fetch is performed + return new FetchCommand() { + @Override + public FetchCommand from(URIish urIish, List list) { + fetchCommand.from(urIish, list); + return this; + } + + @Override + public FetchCommand prune() { + fetchCommand.prune(true); + return this; + } + + @Override + public FetchCommand prune(boolean b) { + fetchCommand.prune(b); + return this; + } + + @Override + public FetchCommand shallow(boolean b) { + fetchCommand.shallow(b); + return this; + } + + @Override + public FetchCommand timeout(Integer integer) { + fetchCommand.timeout(integer); + return this; + } + + @Override + public FetchCommand tags(boolean b) { + fetchCommand.tags(b); + return this; + } + + @Override + public FetchCommand depth(Integer integer) { + fetchCommand.depth(integer); + return this; + } + + @Override + public void execute() throws GitException, InterruptedException { + fetchCommand.execute(); + try { + //Now update the repo with new commits + sharedSampleRepo.write("file3", "New"); + sharedSampleRepo.git("add", "file3"); + sharedSampleRepo.git("commit", "--all", "--message=inbetween"); + } catch (Exception e) { + throw new GitException(e); + } + } + }; + } + } + + private boolean isWindows() { + return File.pathSeparatorChar == ';'; + } +} diff --git a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTrivialTest.java b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTrivialTest.java new file mode 100644 index 0000000000..aa9793d1a4 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTrivialTest.java @@ -0,0 +1,200 @@ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserRemoteConfig; + +import java.util.ArrayList; +import java.util.List; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.eclipse.jgit.transport.RefSpec; +import org.junit.Test; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; + +public class AbstractGitSCMSourceTrivialTest { + + private AbstractGitSCMSource gitSCMSource = null; + + private final String expectedCredentialsId = "expected-credentials-id"; + + private final String expectedIncludes = "*master release* fe?ture substring"; + private final String expectedExcludes = "release bugfix*"; + + private final String expectedRemote = "origin"; + + private final String expectedRefSpec = "+refs/heads/*:refs/remotes/origin/*"; + private final List expectedRefSpecs = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + if (expectedRefSpecs.isEmpty()) { + expectedRefSpecs.add(new RefSpec(expectedRefSpec)); + } + gitSCMSource = new AbstractGitSCMSourceImpl(); + } + + @Test + public void basicTestIsExcluded() { + AbstractGitSCMSource abstractGitSCMSource = mock(AbstractGitSCMSource.class); + + when(abstractGitSCMSource.getIncludes()).thenReturn("*master release* fe?ture"); + when(abstractGitSCMSource.getExcludes()).thenReturn("release bugfix*"); + when(abstractGitSCMSource.isExcluded(Mockito.anyString())).thenCallRealMethod(); + + assertFalse(abstractGitSCMSource.isExcluded("master")); + assertFalse(abstractGitSCMSource.isExcluded("remote/master")); + assertFalse(abstractGitSCMSource.isExcluded("release/X.Y")); + assertFalse(abstractGitSCMSource.isExcluded("releaseX.Y")); + assertFalse(abstractGitSCMSource.isExcluded("fe?ture")); + assertTrue(abstractGitSCMSource.isExcluded("feature")); + assertTrue(abstractGitSCMSource.isExcluded("release")); + assertTrue(abstractGitSCMSource.isExcluded("bugfix")); + assertTrue(abstractGitSCMSource.isExcluded("bugfix/test")); + assertTrue(abstractGitSCMSource.isExcluded("test")); + + when(abstractGitSCMSource.getIncludes()).thenReturn("master feature/*"); + when(abstractGitSCMSource.getExcludes()).thenReturn("feature/*/private"); + assertFalse(abstractGitSCMSource.isExcluded("master")); + assertTrue(abstractGitSCMSource.isExcluded("devel")); + assertFalse(abstractGitSCMSource.isExcluded("feature/spiffy")); + assertTrue(abstractGitSCMSource.isExcluded("feature/spiffy/private")); + } + + @Test + public void testGetCredentialsId() { + assertEquals(expectedCredentialsId, gitSCMSource.getCredentialsId()); + } + + @Test + public void testGetRemote() { + assertEquals(expectedRemote, gitSCMSource.getRemote()); + } + + @Test + public void testGetIncludes() { + assertEquals(expectedIncludes, gitSCMSource.getIncludes()); + } + + @Test + public void testGetExcludes() { + assertEquals(expectedExcludes, gitSCMSource.getExcludes()); + } + + @Test + public void testGetRemoteName() { + assertEquals(expectedRemote, gitSCMSource.getRemoteName()); + } + + @Test + public void testGetRefSpecs() { + assertEquals(expectedRefSpecs, gitSCMSource.getRefSpecs()); + } + + @Test + public void testIsExcluded() { + assertFalse(gitSCMSource.isExcluded("master")); + assertFalse(gitSCMSource.isExcluded("remote/master")); + assertFalse(gitSCMSource.isExcluded("release/X.Y")); + assertFalse(gitSCMSource.isExcluded("releaseX.Y")); + assertFalse(gitSCMSource.isExcluded("fe?ture")); + assertFalse(gitSCMSource.isExcluded("substring")); + + // Excluded because they don't match the inclusion strings + assertTrue(gitSCMSource.isExcluded("feature")); // '?' is not a wildcard + assertTrue(gitSCMSource.isExcluded("test")); + assertTrue(gitSCMSource.isExcluded("foo/substring")); + assertTrue(gitSCMSource.isExcluded("substring/end")); + assertTrue(gitSCMSource.isExcluded("substring1")); + assertTrue(gitSCMSource.isExcluded("remote/substring2")); + assertTrue(gitSCMSource.isExcluded("origin/substring")); + + // Excluded because they match an exclusion string + assertTrue(gitSCMSource.isExcluded("release")); + assertTrue(gitSCMSource.isExcluded("bugfix")); + assertTrue(gitSCMSource.isExcluded("bugfix/test")); + } + + @Test + public void testGetRemoteConfigs() { + List remoteConfigs = gitSCMSource.getRemoteConfigs(); + assertEquals(expectedRemote, remoteConfigs.get(0).getName()); + assertEquals(expectedRefSpec, remoteConfigs.get(0).getRefspec()); + assertEquals("Wrong number of entries in remoteConfigs", 1, remoteConfigs.size()); + } + + @Test + public void testBuild() { + final String expectedBranchName = "origin/master"; + SCMHead head = new SCMHead(expectedBranchName); + SCMRevision revision = new SCMRevisionImpl(head); + GitSCM gitSCM = (GitSCM) gitSCMSource.build(head, revision); + + List remoteConfigs = gitSCM.getUserRemoteConfigs(); + assertEquals(expectedRemote, remoteConfigs.get(0).getName()); + assertEquals(expectedRefSpec, remoteConfigs.get(0).getRefspec()); + assertEquals("Wrong number of entries in remoteConfigs", 1, remoteConfigs.size()); + + List branches = gitSCM.getBranches(); + assertEquals(expectedBranchName, branches.get(0).getName()); + assertEquals("Wrong number of branches", 1, branches.size()); + } + + @Test + public void equalsContractSCMRevisionImpl() { + EqualsVerifier.forClass(AbstractGitSCMSource.SCMRevisionImpl.class) + .usingGetClass() + .verify(); + } + + public class AbstractGitSCMSourceImpl extends AbstractGitSCMSource { + + public AbstractGitSCMSourceImpl() { + setId("AbstractGitSCMSourceImpl-id"); + } + + public String getCredentialsId() { + return expectedCredentialsId; + } + + public String getRemote() { + return expectedRemote; + } + + public String getIncludes() { + return expectedIncludes; + } + + public String getExcludes() { + return expectedExcludes; + } + + public List getRefSpecs() { + return expectedRefSpecs; + } + } + + private class SCMRevisionImpl extends SCMRevision { + + protected SCMRevisionImpl(@NonNull SCMHead scmh) { + super(scmh); + } + + @Override + public boolean equals(Object o) { + throw new UnsupportedOperationException("Intentionally unimplemented"); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException("Intentionally unimplemented"); + } + } + +} diff --git a/src/test/java/jenkins/plugins/git/BrowsersJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/BrowsersJCasCCompatibilityTest.java new file mode 100644 index 0000000000..c1370ae3d9 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/BrowsersJCasCCompatibilityTest.java @@ -0,0 +1,253 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.browser.AssemblaWeb; +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.browser.CGit; +import hudson.plugins.git.browser.FisheyeGitRepositoryBrowser; +import hudson.plugins.git.browser.GitBlitRepositoryBrowser; +import hudson.plugins.git.browser.GitLab; +import hudson.plugins.git.browser.GitList; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.browser.GitWeb; +import hudson.plugins.git.browser.GithubWeb; +import hudson.plugins.git.browser.Gitiles; +import hudson.plugins.git.browser.GitoriousWeb; +import hudson.plugins.git.browser.GogsGit; +import hudson.plugins.git.browser.KilnGit; +import hudson.plugins.git.browser.Phabricator; +import hudson.plugins.git.browser.RedmineWeb; +import hudson.plugins.git.browser.RhodeCode; +import hudson.plugins.git.browser.Stash; +import hudson.plugins.git.browser.TFS2013GitRepositoryBrowser; +import hudson.plugins.git.browser.ViewGitWeb; +import hudson.scm.SCM; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; +import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; +import org.jenkinsci.plugins.workflow.libs.SCMRetriever; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class BrowsersJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + final List libraries = GlobalLibraries.get().getLibraries(); + assertThat(libraries, containsInAnyOrder( + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withAssembla")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withFisheye")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withKiln")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withMic")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withBitbucket")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withCGit")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGithub")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitiles")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitlab")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitlist")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitorious")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitweb")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGogsgit")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withPhab")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withRedmine")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withRhodecode")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withStash")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withViewgit")) + ), + allOf( + instanceOf(LibraryConfiguration.class), + hasProperty("name", equalTo("withGitlib")) + ) + )); + + final List browsers = new ArrayList<>(); + for (LibraryConfiguration library : libraries) { + final String errorMessage = String.format("Error checking library %s", library.getName()); + final LibraryRetriever retriever = library.getRetriever(); + assertThat(errorMessage, retriever, instanceOf(SCMRetriever.class)); + final SCM scm = ((SCMRetriever) retriever).getScm(); + assertThat(errorMessage, scm, instanceOf(GitSCM.class)); + final GitSCM gitSCM = (GitSCM)scm; + assertNotNull(errorMessage, gitSCM.getBrowser()); + browsers.add(gitSCM.getBrowser()); + } + + assertEquals(libraries.size(), browsers.size()); + assertThat(browsers, containsInAnyOrder( + // AssemblaWeb + allOf( + instanceOf(AssemblaWeb.class), + hasProperty("repoUrl", equalTo("http://url.assembla")) + ), + // FishEye + allOf( + instanceOf(FisheyeGitRepositoryBrowser.class), + hasProperty("repoUrl", equalTo("http://url.fishEye/browse/foobar")) + ), + // Kiln + allOf( + instanceOf(KilnGit.class), + hasProperty("repoUrl", equalTo("http://url.kiln")) + ), + // Microsoft Team Foundation Server/Visual Studio Team Services + allOf( + instanceOf(TFS2013GitRepositoryBrowser.class), + hasProperty("repoUrl", equalTo("http://url.mic/_git/foobar/")) + ), + // bitbucketweb + allOf( + instanceOf(BitbucketWeb.class), + hasProperty("repoUrl", equalTo("http://url.bitbucket")) + ), + // cgit + allOf( + instanceOf(CGit.class), + hasProperty("repoUrl", equalTo("http://url.cgit")) + ), + // gitblit + allOf( + instanceOf(GitBlitRepositoryBrowser.class), + hasProperty("repoUrl", equalTo("http://url.gitlib")), + hasProperty("projectName", equalTo("my_project")) + ), + // githubweb + allOf( + instanceOf(GithubWeb.class), + hasProperty("repoUrl", equalTo("http://github.com")) + ), + // gitiles + allOf( + instanceOf(Gitiles.class), + hasProperty("repoUrl", equalTo("http://url.gitiles")) + ), + // gitlab + allOf( + instanceOf(GitLab.class), + // TODO This property fails in CI but succeeds in local. Meanwhile, it's tested in GitLabConfiguratorTest + // hasProperty("version", equalTo(1.0)), + hasProperty("repoUrl", equalTo("http://gitlab.com")) + ), + // gitlist + allOf( + instanceOf(GitList.class), + hasProperty("repoUrl", equalTo("http://url.gitlist")) + ), + // gitoriousweb + allOf( + instanceOf(GitoriousWeb.class), + hasProperty("repoUrl", equalTo("http://url.gitorious")) + ), + // gitweb + allOf( + instanceOf(GitWeb.class), + hasProperty("repoUrl", equalTo("http://url.gitweb")) + ), + // gogs + allOf( + instanceOf(GogsGit.class), + hasProperty("repoUrl", equalTo("http://url.gogs")) + ), + // phabricator + allOf( + instanceOf(Phabricator.class), + hasProperty("repoUrl", equalTo("http://url.phabricator")), + hasProperty("repo", equalTo("my_repository")) + ), + // redmineweb + allOf( + instanceOf(RedmineWeb.class), + hasProperty("repoUrl", equalTo("http://url.redmineweb")) + ), + // rhodecode + allOf( + instanceOf(RhodeCode.class), + hasProperty("repoUrl", equalTo("http://url.rhodecode")) + ), + // stash + allOf( + instanceOf(Stash.class), + hasProperty("repoUrl", equalTo("http://url.stash")) + ), + // viewgit + allOf( + instanceOf(ViewGitWeb.class), + hasProperty("repoUrl", equalTo("http://url.viewgit")), + hasProperty("projectName", equalTo("my_other_project")) + ) + )); + } + + @Override + protected String stringInLogExpected() { + return "Setting class hudson.plugins.git.browser.GitBlitRepositoryBrowser.repoUrl = http://url.gitlib"; + } + + @Override + protected String configResource() { + return "browsers-casc.yaml"; + } +} diff --git a/src/test/java/jenkins/plugins/git/CliGitCommand.java b/src/test/java/jenkins/plugins/git/CliGitCommand.java new file mode 100644 index 0000000000..8e61a13eda --- /dev/null +++ b/src/test/java/jenkins/plugins/git/CliGitCommand.java @@ -0,0 +1,157 @@ +/* + * The MIT License + * + * Copyright 2016-2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.TaskListener; +import hudson.util.ArgumentListBuilder; +import hudson.util.StreamTaskListener; +import hudson.plugins.git.GitException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Repository; +import static org.hamcrest.Matchers.hasItems; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Assert; +import static org.junit.Assert.assertThat; + +/** + * Run a command line git command, return output as array of String, optionally + * assert on contents of command output. + * + * @author Mark Waite + */ +public class CliGitCommand { + + private final TaskListener listener; + private final transient Launcher launcher; + private final EnvVars env; + private final File dir; + private String[] output; + private ArgumentListBuilder args; + + public CliGitCommand(GitClient client, String... arguments) { + args = new ArgumentListBuilder("git"); + args.add(arguments); + listener = StreamTaskListener.NULL; + launcher = new Launcher.LocalLauncher(listener); + env = new EnvVars(); + if (client != null) { + try (Repository repo = client.getRepository()) { + dir = repo.getWorkTree(); + } + } else { + dir = new File("."); + } + } + + public String[] run(String... arguments) throws IOException, InterruptedException { + args = new ArgumentListBuilder("git"); + args.add(arguments); + return run(true); + } + + public String[] run() throws IOException, InterruptedException { + return run(true); + } + + private String[] run(boolean assertProcessStatus) throws IOException, InterruptedException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + ByteArrayOutputStream bytesErr = new ByteArrayOutputStream(); + Launcher.ProcStarter p = launcher.launch().cmds(args).envs(env).stdout(bytesOut).stderr(bytesErr).pwd(dir); + int status = p.start().joinWithTimeout(1, TimeUnit.MINUTES, listener); + String result = bytesOut.toString("UTF-8"); + if (bytesErr.size() > 0) { + result = result + "\nstderr not empty:\n" + bytesErr.toString("UTF-8"); + } + output = result.split("[\\n\\r]"); + if (assertProcessStatus) { + Assert.assertEquals(args.toString() + " command failed and reported '" + Arrays.toString(output) + "'", 0, status); + } + return output; + } + + public void assertOutputContains(String... expectedRegExes) { + List notFound = new ArrayList<>(); + boolean modified = notFound.addAll(Arrays.asList(expectedRegExes)); + Assert.assertTrue("Missing regular expressions in assertion", modified); + for (String line : output) { + for (Iterator iterator = notFound.iterator(); iterator.hasNext();) { + String regex = iterator.next(); + if (line.matches(regex)) { + iterator.remove(); + } + } + } + if (!notFound.isEmpty()) { + Assert.fail(Arrays.toString(output) + " did not match all strings in notFound: " + Arrays.toString(expectedRegExes)); + } + } + + private String[] runWithoutAssert(String... arguments) throws IOException, InterruptedException { + args = new ArgumentListBuilder("git"); + args.add(arguments); + return run(false); + } + + private void setConfigIfEmpty(String configName, String value) throws Exception { + String[] cmdOutput = runWithoutAssert("config", "--global", configName); + if (cmdOutput == null || cmdOutput[0].isEmpty() || cmdOutput[0].equals("[]")) { + /* Set config value globally */ + cmdOutput = run("config", "--global", configName, value); + assertThat(Arrays.asList(cmdOutput), hasItems("")); + /* Read config value */ + cmdOutput = run("config", "--global", configName); + if (cmdOutput == null || cmdOutput[0].isEmpty() || !cmdOutput[0].equals(value)) { + throw new GitException("ERROR: git config --global " + configName + " reported '" + cmdOutput[0] + "' instead of '" + value + "'"); + } + } + } + + /** + * Set git config values for user.name and user.email if they are not + * already set. Many tests assume that "git commit" can be called without + * failure, but a newly installed user account does not necessarily have + * values assigned for user.name and user.email. This method checks the + * existing values when run in a Jenkins job, and if they are not set, + * assigns default values. If the + * values are already set, they are unchanged. + * @throws Exception on error + */ + public void setDefaults() throws Exception { + if (System.getenv("JENKINS_URL") != null && System.getenv("BUILD_NUMBER") != null) { + /* We're in a Jenkins agent environment */ + setConfigIfEmpty("user.name", "Name From Git-Plugin-Test"); + setConfigIfEmpty("user.email", "email.from.git.plugin.test@example.com"); + } + } +} diff --git a/src/test/java/jenkins/plugins/git/GitBranchSCMHeadTest.java b/src/test/java/jenkins/plugins/git/GitBranchSCMHeadTest.java new file mode 100644 index 0000000000..8ea7fd3471 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitBranchSCMHeadTest.java @@ -0,0 +1,84 @@ +package jenkins.plugins.git; + +import hudson.FilePath; +import hudson.Functions; +import hudson.model.Queue; +import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; + +public class GitBranchSCMHeadTest { + + @Rule + public JenkinsRule j = new JenkinsRule() { + @Override + public void before() throws Throwable { + if (!Functions.isWindows() && "testMigrationNoBuildStorm".equals(this.getTestDescription().getMethodName())) { + URL res = getClass().getResource("/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm_repositories.zip"); + final File path = new File("/tmp/JENKINS-48061"); + if (path.exists()) { + if (path.isDirectory()) { + FileUtils.deleteDirectory(path); + } else { + path.delete(); + } + } + + new FilePath(new File(res.toURI())).unzip(new FilePath(path.getParentFile())); + } + super.before(); + } + }; + + @After + public void removeRepos() throws IOException { + final File path = new File("/tmp/JENKINS-48061"); + if (path.exists() && path.isDirectory()) { + FileUtils.deleteDirectory(path); + } + } + + + @Issue("JENKINS-48061") + @Test + @LocalData + public void testMigrationNoBuildStorm() throws Exception { + assumeFalse(Functions.isWindows()); + final WorkflowMultiBranchProject job = j.jenkins.getItemByFullName("job", WorkflowMultiBranchProject.class); + assertEquals(4, job.getItems().size()); + WorkflowJob master = job.getItem("master"); + assertEquals(1, master.getBuilds().size()); + WorkflowJob dev = job.getItem("dev"); + assertEquals(1, dev.getBuilds().size()); + WorkflowJob v4 = job.getItem("v4"); + assertEquals(0, v4.getBuilds().size()); + + final Queue.Item item = job.scheduleBuild2(0); + assertNotNull(item); + item.getFuture().waitForStart(); + j.waitUntilNoActivity(); + + assertEquals(4, job.getItems().size()); + master = job.getItem("master"); + assertEquals(1, master.getBuilds().size()); + dev = job.getItem("dev"); + assertEquals(1, dev.getBuilds().size()); + v4 = job.getItem("v4"); + assertEquals(0, v4.getBuilds().size()); + } + +} \ No newline at end of file diff --git a/src/test/java/jenkins/plugins/git/GitJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/GitJCasCCompatibilityTest.java new file mode 100644 index 0000000000..c3c6db2a5a --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitJCasCCompatibilityTest.java @@ -0,0 +1,40 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.scm.SCM; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import org.hamcrest.CoreMatchers; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; +import org.jenkinsci.plugins.workflow.libs.SCMRetriever; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import java.util.Collection; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class GitJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + LibraryRetriever retriever = GlobalLibraries.get().getLibraries().get(0).getRetriever(); + assertThat(retriever, CoreMatchers.instanceOf(SCMRetriever.class)); + SCM scm = ((SCMRetriever) retriever).getScm(); + assertThat(scm, CoreMatchers.instanceOf(GitSCM.class)); + + Collection submodulesConfig = ((GitSCM) scm).getSubmoduleCfg(); + + assertThat(submodulesConfig.size(), is(2)); + assertTrue(submodulesConfig.stream().anyMatch(m -> m.getSubmoduleName().equals("submodule-1"))); + assertTrue(submodulesConfig.stream().anyMatch(m -> m.getSubmoduleName().equals("submodule-2"))); + assertTrue(submodulesConfig.stream().anyMatch(m -> m.getBranchesString().equals("mybranch-1,mybranch-2"))); + assertTrue(submodulesConfig.stream().anyMatch(m -> m.getBranchesString().equals("mybranch-3,mybranch-4"))); + } + + @Override + protected String stringInLogExpected() { + return "Setting class hudson.plugins.git.SubmoduleConfig.branches = [mybranch-3, mybranch-4]"; + } +} diff --git a/src/test/java/jenkins/plugins/git/GitRemoteHeadRefActionTest.java b/src/test/java/jenkins/plugins/git/GitRemoteHeadRefActionTest.java new file mode 100644 index 0000000000..f9f1f4c1c8 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitRemoteHeadRefActionTest.java @@ -0,0 +1,14 @@ +package jenkins.plugins.git; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class GitRemoteHeadRefActionTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(GitRemoteHeadRefAction.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMBuilderTest.java b/src/test/java/jenkins/plugins/git/GitSCMBuilderTest.java new file mode 100644 index 0000000000..c3de7fc8a9 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMBuilderTest.java @@ -0,0 +1,598 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.GithubWeb; +import hudson.plugins.git.extensions.impl.AuthorInChangelog; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.plugins.git.extensions.impl.CleanCheckout; +import hudson.plugins.git.extensions.impl.CloneOption; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.plugins.git.util.InverseBuildChooser; +import java.util.Collections; +import jenkins.scm.api.SCMHead; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Test; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class GitSCMBuilderTest { + + private GitSCMBuilder instance = new GitSCMBuilder<>( + new SCMHead("master"), + null, + "http://git.test/repo.git", + null); + + @Test + public void build() throws Exception { + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withRevision() throws Exception { + instance.withExtension(new BuildChooserSetting(new InverseBuildChooser())); + GitSCM scm = instance.build(); + assertThat(scm.getExtensions().get(BuildChooserSetting.class), notNullValue()); + assertThat(scm.getExtensions().get(BuildChooserSetting.class).getBuildChooser(), + instanceOf(InverseBuildChooser.class)); + instance.withRevision( + new AbstractGitSCMSource.SCMRevisionImpl(instance.head(), "3f0b897057d8b43d3b9ff55e3fdefbb021493470")); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions().get(BuildChooserSetting.class), notNullValue()); + assertThat(scm.getExtensions().get(BuildChooserSetting.class).getBuildChooser(), + instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + assertThat(scm.getExtensions().get(BuildChooserSetting.class).getBuildChooser() + .getCandidateRevisions(false, null, (GitClient) null, null, null, null).iterator().next() + .getSha1String(), is("3f0b897057d8b43d3b9ff55e3fdefbb021493470")); + } + + @Test + public void withBrowser() throws Exception { + instance.withBrowser(new GithubWeb("http://git.test/repo.git")); + assertThat(instance.browser(), is(instanceOf(GithubWeb.class))); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(instanceOf(GithubWeb.class))); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withCredentials() throws Exception { + instance.withCredentials("example-id"); + assertThat(instance.credentialsId(), is("example-id")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is("example-id"))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withExtension() throws Exception { + instance.withExtension(new AuthorInChangelog()); + assertThat(instance.extensions(), contains(instanceOf(AuthorInChangelog.class))); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class) + )); + + // repeated calls build up new extensions + instance.withExtension(new LocalBranch("**")); + assertThat(instance.extensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("**"))) + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("**"))) + )); + + // repeated calls re-define up existing extensions + instance.withExtension(new LocalBranch("master")); + assertThat(instance.extensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("master"))) + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("master"))) + )); + } + + @Test + public void withExtensions() throws Exception { + instance.withExtensions(new AuthorInChangelog()); + assertThat(instance.extensions(), contains(instanceOf(AuthorInChangelog.class))); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class) + )); + + // repeated calls build up extensions + instance.withExtensions(new CleanCheckout()); + assertThat(instance.extensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(CleanCheckout.class) + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class), + instanceOf(CleanCheckout.class) + )); + + instance.withExtension(new LocalBranch("**")); + assertThat(instance.extensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(CleanCheckout.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("**"))) + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class), + instanceOf(CleanCheckout.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("**"))) + )); + + // repeated calls re-define up existing extensions + instance.withExtension(new LocalBranch("master")); + assertThat(instance.extensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(CleanCheckout.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("master"))) + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), containsInAnyOrder( + instanceOf(AuthorInChangelog.class), + instanceOf(GitSCMSourceDefaults.class), + instanceOf(CleanCheckout.class), + allOf(instanceOf(LocalBranch.class), hasProperty("localBranch", is("master"))) + )); + } + + @Test + public void withGitTool() throws Exception { + instance.withGitTool("git"); + assertThat(instance.gitTool(), is("git")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is("git")); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withRefSpecAndCloneOption() throws Exception { + instance.withRefSpec("+refs/heads/master:refs/remotes/@{remote}/master"); + instance.withExtension(new CloneOption(false, false, null, null)); + assertThat(instance.refSpecs(), contains("+refs/heads/master:refs/remotes/@{remote}/master")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(CloneOption.class) + )); + } + + @Test + public void withRefSpec() throws Exception { + instance.withRefSpec("+refs/heads/master:refs/remotes/@{remote}/master"); + assertThat(instance.refSpecs(), contains("+refs/heads/master:refs/remotes/@{remote}/master")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + // repeated calls build up + instance.withRefSpec("+refs/heads/feature:refs/remotes/@{remote}/feature"); + assertThat(instance.refSpecs(), containsInAnyOrder( + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/feature:refs/remotes/@{remote}/feature" + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), containsInAnyOrder( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master " + + "+refs/heads/feature:refs/remotes/origin/feature")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + // repeated calls build up but remote configs de-duplicated + instance.withRefSpec("+refs/heads/master:refs/remotes/@{remote}/master"); + assertThat(instance.refSpecs(), containsInAnyOrder( + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/feature:refs/remotes/@{remote}/feature", + "+refs/heads/master:refs/remotes/@{remote}/master" + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), containsInAnyOrder( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master " + + "+refs/heads/feature:refs/remotes/origin/feature")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + // de-duplication is applied after template substitution + instance.withRefSpec("+refs/heads/master:refs/remotes/origin/master"); + assertThat(instance.refSpecs(), containsInAnyOrder( + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/feature:refs/remotes/@{remote}/feature", + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/master:refs/remotes/origin/master" + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), containsInAnyOrder( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master " + + "+refs/heads/feature:refs/remotes/origin/feature")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withRefSpecs() throws Exception { + instance.withRefSpecs(Collections.singletonList("+refs/heads/master:refs/remotes/@{remote}/master")); + assertThat(instance.refSpecs(), contains("+refs/heads/master:refs/remotes/@{remote}/master")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + // repeated calls accumulate + instance.withRefSpecs(Collections.singletonList("+refs/heads/feature:refs/remotes/@{remote}/feature")); + assertThat(instance.refSpecs(), contains( + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/feature:refs/remotes/@{remote}/feature" + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master " + + "+refs/heads/feature:refs/remotes/origin/feature")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + // empty list is no-op + instance.withRefSpecs(Collections.emptyList()); + assertThat(instance.refSpecs(), contains( + "+refs/heads/master:refs/remotes/@{remote}/master", + "+refs/heads/feature:refs/remotes/@{remote}/feature" + )); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/origin/master " + + "+refs/heads/feature:refs/remotes/origin/feature")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withoutRefSpecs() throws Exception { + instance.withRefSpecs(Collections.singletonList("+refs/heads/feature:refs/remotes/@{remote}/feature")); + assumeThat(instance.refSpecs(), not(contains( + "+refs/heads/*:refs/remotes/@{remote}/*" + ))); + instance.withoutRefSpecs(); + assertThat(instance.refSpecs(), contains("+refs/heads/*:refs/remotes/@{remote}/*")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withRemote() throws Exception { + instance.withRemote("http://git.test/my-repo.git"); + assertThat(instance.remote(), is("http://git.test/my-repo.git")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/my-repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withRemoteName() throws Exception { + instance.withRemoteName("my-remote"); + assertThat(instance.remoteName(), is("my-remote")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), contains(allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("my-remote")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/my-remote/*")), + hasProperty("credentialsId", is(nullValue()))) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + + @Test + public void withAdditionalRemote() throws Exception { + instance.withAdditionalRemote("upstream", "http://git.test/upstream.git", + "+refs/heads/master:refs/remotes/@{remote}/master"); + assertThat(instance.additionalRemoteNames(), contains("upstream")); + assertThat(instance.additionalRemote("upstream"), is("http://git.test/upstream.git")); + assertThat(instance.additionalRemoteRefSpecs("upstream"), contains( + "+refs/heads/master:refs/remotes/@{remote}/master")); + GitSCM scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), containsInAnyOrder( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue())) + ), + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/upstream.git")), + hasProperty("name", is("upstream")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/upstream/master")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + + instance.withAdditionalRemote("production", "http://git.test/production.git"); + assertThat(instance.additionalRemoteNames(), containsInAnyOrder("upstream", "production")); + assertThat(instance.additionalRemote("upstream"), is("http://git.test/upstream.git")); + assertThat(instance.additionalRemoteRefSpecs("upstream"), contains( + "+refs/heads/master:refs/remotes/@{remote}/master")); + assertThat(instance.additionalRemote("production"), is("http://git.test/production.git")); + assertThat(instance.additionalRemoteRefSpecs("production"), contains( + "+refs/heads/*:refs/remotes/@{remote}/*")); + scm = instance.build(); + assertThat(scm.getBrowser(), is(nullValue())); + assertThat(scm.getUserRemoteConfigs(), containsInAnyOrder( + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/repo.git")), + hasProperty("name", is("origin")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/origin/*")), + hasProperty("credentialsId", is(nullValue())) + ), + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/upstream.git")), + hasProperty("name", is("upstream")), + hasProperty("refspec", is("+refs/heads/master:refs/remotes/upstream/master")), + hasProperty("credentialsId", is(nullValue())) + ), + allOf( + instanceOf(UserRemoteConfig.class), + hasProperty("url", is("http://git.test/production.git")), + hasProperty("name", is("production")), + hasProperty("refspec", is("+refs/heads/*:refs/remotes/production/*")), + hasProperty("credentialsId", is(nullValue())) + ) + )); + assertThat(scm.getGitTool(), is(nullValue())); + assertThat(scm.getExtensions(), contains( + instanceOf(GitSCMSourceDefaults.class) + )); + } + +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMFileSystemTest.java b/src/test/java/jenkins/plugins/git/GitSCMFileSystemTest.java new file mode 100644 index 0000000000..dd23287cd1 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMFileSystemTest.java @@ -0,0 +1,368 @@ +/* + * The MIT License + * + * Copyright (c) 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package jenkins.plugins.git; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.GitException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; +import jenkins.plugins.git.CliGitCommand; +import jenkins.scm.api.SCMFile; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceDescriptor; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link AbstractGitSCMSource} + */ +public class GitSCMFileSystemTest { + + @ClassRule + public static JenkinsRule r = new JenkinsRule(); + + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + private final static String GIT_2_6_0_TAG = "git-2.6.0"; + private final static String GIT_2_6_1_TAG = "git-2.6.1"; + + /* This test requires the tag git-2.6.1 and git-2.6.0. If you're working from a + * forked copy of the repository and your fork was created before the + * git-2.6.1 plugin release, you may not have that tag in your fork. + * If you do not have that tag, you will need to include that tag in + * your fork. You can do that with the commands: + * + * $ git fetch --tags https://github.com/jenkinsci/git-plugin + * $ git push --tags origin + */ + @BeforeClass + public static void confirmTagsAvailable() throws Exception { + File gitDir = new File("."); + GitClient client = Git.with(TaskListener.NULL, new EnvVars()).in(gitDir).using("jgit").getClient(); + + String[] tags = { GIT_2_6_0_TAG, GIT_2_6_1_TAG }; + for (String tag : tags) { + ObjectId tagId; + try { + tagId = client.revParse(tag); + } catch (GitException ge) { + CliGitCommand gitCmd = new CliGitCommand(null); + gitCmd.run("fetch", "--tags", "https://github.com/jenkinsci/git-plugin"); + tagId = client.revParse(tag); /* throws if tag not available */ + } + } + } + + @Test + public void ofSource_Smokes() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMFileSystem fs = SCMFileSystem.of(source, new GitBranchSCMHead("dev")); + assertThat(fs, notNullValue()); + SCMFile root = fs.getRoot(); + assertThat(root, notNullValue()); + assertTrue(root.isRoot()); + // assertTrue(root.isDirectory()); // IllegalArgumentException + // assertTrue(root.exists()); // IllegalArgumentException + // assertFalse(root.isFile()); // IllegalArgumentException + Iterable children = root.children(); + Iterator iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile file = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(file.getName(), is("file")); + assertThat(file.contentAsString(), is("modified")); + } + + @Test + public void ofSourceRevision() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMRevision revision = source.fetch(new SCMHead("dev"), null); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMFileSystem fs = SCMFileSystem.of(source, new SCMHead("dev"), revision); + assertThat(fs, notNullValue()); + assertThat(fs.getRoot(), notNullValue()); + Iterable children = fs.getRoot().children(); + Iterator iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile file = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(file.getName(), is("file")); + assertThat(file.contentAsString(), is("")); + } + + @Test + public void ofSourceRevision_GitBranchSCMHead() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMRevision revision = source.fetch(new GitBranchSCMHead("dev"), null); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMFileSystem fs = SCMFileSystem.of(source, new GitBranchSCMHead("dev"), revision); + assertThat(fs, notNullValue()); + assertThat(fs.getRoot(), notNullValue()); + Iterable children = fs.getRoot().children(); + Iterator iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile file = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(file.getName(), is("file")); + assertThat(file.contentAsString(), is("")); + } + + @Issue("JENKINS-42817") + @Test + public void slashyBranches() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "bug/JENKINS-42817"); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMFileSystem fs = SCMFileSystem.of(r.createFreeStyleProject(), new GitSCM(GitSCM.createRepoList(sampleRepo.toString(), null), Collections.singletonList(new BranchSpec("*/bug/JENKINS-42817")), false, Collections.emptyList(), null, null, Collections.emptyList())); + assertThat(fs, notNullValue()); + SCMFile root = fs.getRoot(); + assertThat(root, notNullValue()); + assertTrue(root.isRoot()); + Iterable children = root.children(); + Iterator iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile file = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(file.getName(), is("file")); + assertThat(file.contentAsString(), is("modified")); + } + + @Test + public void lastModified_Smokes() throws Exception { + Assume.assumeTrue("Windows file system last modify dates not trustworthy", !isWindows()); + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMRevision revision = source.fetch(new GitBranchSCMHead("dev"), null); + sampleRepo.write("file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + final long fileSystemAllowedOffset = 1500; + SCMFileSystem fs = SCMFileSystem.of(source, new SCMHead("dev"), revision); + long currentTime = System.currentTimeMillis(); + long lastModified = fs.lastModified(); + assertThat(lastModified, greaterThanOrEqualTo(currentTime - fileSystemAllowedOffset)); + assertThat(lastModified, lessThanOrEqualTo(currentTime + fileSystemAllowedOffset)); + SCMFile file = fs.getRoot().child("file"); + currentTime = System.currentTimeMillis(); + lastModified = file.lastModified(); + assertThat(lastModified, greaterThanOrEqualTo(currentTime - fileSystemAllowedOffset)); + assertThat(lastModified, lessThanOrEqualTo(currentTime + fileSystemAllowedOffset)); + } + + @Test + public void directoryTraversal() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.mkdirs("dir/subdir"); + sampleRepo.git("mv", "file", "dir/subdir/file"); + sampleRepo.write("dir/subdir/file", "modified"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMFileSystem fs = SCMFileSystem.of(source, new SCMHead("dev")); + assertThat(fs, notNullValue()); + assertThat(fs.getRoot(), notNullValue()); + Iterable children = fs.getRoot().children(); + Iterator iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile dir = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(dir.getName(), is("dir")); + assertThat(dir.getType(), is(SCMFile.Type.DIRECTORY)); + children = dir.children(); + iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile subdir = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(subdir.getName(), is("subdir")); + assertThat(subdir.getType(), is(SCMFile.Type.DIRECTORY)); + children = subdir.children(); + iterator = children.iterator(); + assertThat(iterator.hasNext(), is(true)); + SCMFile file = iterator.next(); + assertThat(iterator.hasNext(), is(false)); + assertThat(file.getName(), is("file")); + assertThat(file.contentAsString(), is("modified")); + } + + @Test + public void mixedContent() throws Exception { + sampleRepo.init(); + sampleRepo.git("checkout", "-b", "dev"); + sampleRepo.write("file", "modified"); + sampleRepo.write("file2", "new"); + sampleRepo.git("add", "file2"); + sampleRepo.write("dir/file3", "modified"); + sampleRepo.git("add", "file", "dir/file3"); + sampleRepo.git("commit", "--all", "--message=dev"); + SCMSource source = new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true); + SCMFileSystem fs = SCMFileSystem.of(source, new SCMHead("dev")); + assertThat(fs, notNullValue()); + assertThat(fs.getRoot(), notNullValue()); + Iterable children = fs.getRoot().children(); + Set names = new TreeSet<>(); + SCMFile file = null; + SCMFile file2 = null; + SCMFile dir = null; + for (SCMFile f: children) { + names.add(f.getName()); + switch (f.getName()) { + case "file": + file = f; + break; + case "file2": + file2 = f; + break; + case "dir": + dir = f; + break; + default: + break; + } + } + assertThat(names, containsInAnyOrder(is("file"), is("file2"), is("dir"))); + assertThat(file.getType(), is(SCMFile.Type.REGULAR_FILE)); + assertThat(file2.getType(), is(SCMFile.Type.REGULAR_FILE)); + assertThat(dir.getType(), is(SCMFile.Type.DIRECTORY)); + assertThat(file.contentAsString(), is("modified")); + assertThat(file2.contentAsString(), is("new")); + } + + @Test + public void given_filesystem_when_askingChangesSinceSameRevision_then_changesAreEmpty() throws Exception { + File gitDir = new File("."); + GitClient client = Git.with(TaskListener.NULL, new EnvVars()).in(gitDir).using("git").getClient(); + + ObjectId git261 = client.revParse(GIT_2_6_1_TAG); + AbstractGitSCMSource.SCMRevisionImpl rev261 = + new AbstractGitSCMSource.SCMRevisionImpl(new SCMHead("origin"), git261.getName()); + GitSCMFileSystem gitPlugin261FS = new GitSCMFileSystem(client, "origin", git261.getName(), rev261); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertFalse(gitPlugin261FS.changesSince(rev261, out)); + assertThat(out.toString(), is("")); + } + + @Test + public void given_filesystem_when_askingChangesSinceOldRevision_then_changesArePopulated() throws Exception { + File gitDir = new File("."); + GitClient client = Git.with(TaskListener.NULL, new EnvVars()).in(gitDir).using("git").getClient(); + + ObjectId git261 = client.revParse(GIT_2_6_1_TAG); + AbstractGitSCMSource.SCMRevisionImpl rev261 = + new AbstractGitSCMSource.SCMRevisionImpl(new SCMHead("origin"), git261.getName()); + GitSCMFileSystem gitPlugin261FS = new GitSCMFileSystem(client, "origin", git261.getName(), rev261); + + ObjectId git260 = client.revParse(GIT_2_6_0_TAG); + AbstractGitSCMSource.SCMRevisionImpl rev260 = + new AbstractGitSCMSource.SCMRevisionImpl(new SCMHead("origin"), git260.getName()); + + assertThat(git260, not(is(git261))); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertTrue(gitPlugin261FS.changesSince(rev260, out)); + assertThat(out.toString(), containsString("prepare release git-2.6.1")); + } + + @Test + public void given_filesystem_when_askingChangesSinceNewRevision_then_changesArePopulatedButEmpty() throws Exception { + File gitDir = new File("."); + GitClient client = Git.with(TaskListener.NULL, new EnvVars()).in(gitDir).using("git").getClient(); + + ObjectId git260 = client.revParse(GIT_2_6_0_TAG); + AbstractGitSCMSource.SCMRevisionImpl rev260 = + new AbstractGitSCMSource.SCMRevisionImpl(new SCMHead("origin"), git260.getName()); + GitSCMFileSystem gitPlugin260FS = new GitSCMFileSystem(client, "origin", git260.getName(), rev260); + + ObjectId git261 = client.revParse(GIT_2_6_1_TAG); + AbstractGitSCMSource.SCMRevisionImpl rev261 = + new AbstractGitSCMSource.SCMRevisionImpl(new SCMHead("origin"), git261.getName()); + GitSCMFileSystem gitPlugin261FS = + new GitSCMFileSystem(client, "origin", git261.getName(), rev261); + assertEquals(git261.getName(), gitPlugin261FS.getRevision().getHash()); + + assertThat(git261, not(is(git260))); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertTrue(gitPlugin260FS.changesSince(rev261, out)); + assertThat(out.toString(), is("")); + } + + @Issue("JENKINS-52964") + @Test + public void filesystem_supports_descriptor() throws Exception { + SCMSourceDescriptor descriptor = r.jenkins.getDescriptorByType(GitSCMSource.DescriptorImpl.class); + assertTrue(SCMFileSystem.supports(descriptor)); + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return java.io.File.pathSeparatorChar==';'; + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/GitSCMJCasCCompatibilityTest.java new file mode 100644 index 0000000000..153b0382a9 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMJCasCCompatibilityTest.java @@ -0,0 +1,28 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.GitSCM; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class GitSCMJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + GitSCM.DescriptorImpl gitSCM = (GitSCM.DescriptorImpl) restartableJenkinsRule.j.jenkins.getScm(GitSCM.class.getSimpleName()); + assertEquals("user_name", gitSCM.getGlobalConfigName()); + assertEquals("me@mail.com", gitSCM.getGlobalConfigEmail()); + assertTrue(gitSCM.isCreateAccountBasedOnEmail()); + } + + @Override + protected String stringInLogExpected() { + return "globalConfigName = user_name"; + } + + @Override + protected String configResource() { + return "gitscm-casc.yaml"; + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMSourceContextTest.java b/src/test/java/jenkins/plugins/git/GitSCMSourceContextTest.java new file mode 100644 index 0000000000..5625009b93 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMSourceContextTest.java @@ -0,0 +1,14 @@ +package jenkins.plugins.git; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class GitSCMSourceContextTest { + + @Test + public void equalsContract_RefNameMapping() { + EqualsVerifier.forClass(GitSCMSourceContext.RefNameMapping.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMSourceDefaultsTest.java b/src/test/java/jenkins/plugins/git/GitSCMSourceDefaultsTest.java new file mode 100644 index 0000000000..d61ec5d72f --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMSourceDefaultsTest.java @@ -0,0 +1,14 @@ +package jenkins.plugins.git; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class GitSCMSourceDefaultsTest { + + @Test + public void equalsContract() { + EqualsVerifier.forClass(GitSCMSourceDefaults.class) + .usingGetClass() + .verify(); + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMSourceTest.java b/src/test/java/jenkins/plugins/git/GitSCMSourceTest.java new file mode 100644 index 0000000000..fe6a82d9c0 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMSourceTest.java @@ -0,0 +1,674 @@ +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Action; +import hudson.model.Item; +import hudson.model.Node; +import hudson.model.TaskListener; +import hudson.model.TopLevelItem; +import hudson.EnvVars; +import hudson.FilePath; +import hudson.plugins.git.GitStatus; +import hudson.plugins.git.GitTool; +import hudson.remoting.Launcher; +import hudson.scm.SCMDescriptor; +import hudson.tools.CommandInstaller; +import hudson.tools.InstallSourceProperty; +import hudson.tools.ToolInstallation; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import hudson.util.StreamTaskListener; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.plugins.git.traits.TagDiscoveryTrait; +import jenkins.scm.api.SCMEventListener; +import jenkins.scm.api.SCMFile; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.SCMSourceDescriptor; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.hamcrest.Matchers; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Collections; +import org.jvnet.hudson.test.TestExtension; +import org.mockito.Mockito; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Robin Müller + */ +public class GitSCMSourceTest { + + public static final String REMOTE = "git@remote:test/project.git"; + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + private GitStatus gitStatus; + + @Before + public void setup() { + gitStatus = new GitStatus(); + } + + @Test + public void testSourceOwnerTriggeredByDoNotifyCommit() throws Exception { + GitSCMSource gitSCMSource = new GitSCMSource("id", REMOTE, "", "*", "", false); + GitSCMSourceOwner scmSourceOwner = setupGitSCMSourceOwner(gitSCMSource); + jenkins.getInstance().add(scmSourceOwner, "gitSourceOwner"); + + gitStatus.doNotifyCommit(mock(HttpServletRequest.class), REMOTE, "master", ""); + + SCMHeadEvent event = + jenkins.getInstance().getExtensionList(SCMEventListener.class).get(SCMEventListenerImpl.class) + .waitSCMHeadEvent(1, TimeUnit.SECONDS); + assertThat(event, notNullValue()); + assertThat((Iterable) event.heads(gitSCMSource).keySet(), hasItem(is(new GitBranchSCMHead("master")))); + verify(scmSourceOwner, times(0)).onSCMSourceUpdated(gitSCMSource); + + } + + private GitSCMSourceOwner setupGitSCMSourceOwner(GitSCMSource gitSCMSource) { + GitSCMSourceOwner owner = mock(GitSCMSourceOwner.class); + when(owner.hasPermission(Item.READ)).thenReturn(true, true, true); + when(owner.getSCMSources()).thenReturn(Collections.singletonList(gitSCMSource)); + return owner; + } + + private interface GitSCMSourceOwner extends TopLevelItem, SCMSourceOwner { + } + + @TestExtension + public static class SCMEventListenerImpl extends SCMEventListener { + + SCMHeadEvent head = null; + + @Override + public void onSCMHeadEvent(SCMHeadEvent event) { + synchronized (this) { + head = event; + notifyAll(); + } + } + + public SCMHeadEvent waitSCMHeadEvent(long timeout, TimeUnit units) + throws TimeoutException, InterruptedException { + long giveUp = System.currentTimeMillis() + units.toMillis(timeout); + while (System.currentTimeMillis() < giveUp) { + synchronized (this) { + SCMHeadEvent h = head; + if (h != null) { + head = null; + return h; + } + wait(Math.max(1L, giveUp - System.currentTimeMillis())); + } + } + throw new TimeoutException(); + } + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetch() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + instance.setOwner(mock(SCMSourceOwner.class)); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + Map result = instance.fetch(SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("manchu"), "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc" + ), + new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ))); + + instance.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + result = instance.fetch(SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("manchu"), "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc" + ))); + + instance.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + result = instance.fetch(SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ))); + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetchWithCriteria() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + instance.setOwner(mock(SCMSourceOwner.class)); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + Map result = instance.fetch(new MySCMSourceCriteria("Jenkinsfile"), + SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ), + new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ))); + result = instance.fetch(new MySCMSourceCriteria("README.md"), + SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("manchu"), "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc" + ))); + + instance.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + result = instance.fetch(new MySCMSourceCriteria("Jenkinsfile"), SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ))); + + instance.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + result = instance.fetch(new MySCMSourceCriteria("Jenkinsfile"), SCMHeadObserver.collect(), null).result(); + assertThat(result.values(), Matchers.containsInAnyOrder( + new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ))); + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetchRevisions() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + instance.setOwner(mock(SCMSourceOwner.class)); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + Set result = instance.fetchRevisions(null, null); + assertThat(result, containsInAnyOrder("foo", "bar", "manchu", "v1.0.0")); + + instance.setTraits(Collections.singletonList(new BranchDiscoveryTrait())); + result = instance.fetchRevisions(null, null); + assertThat(result, containsInAnyOrder("foo", "bar", "manchu")); + + instance.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + result = instance.fetchRevisions(null, null); + assertThat(result, containsInAnyOrder("v1.0.0")); + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetchRevision() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + instance.setOwner(mock(SCMSourceOwner.class)); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + assertThat(instance.fetch(new SCMHead("foo"), null), + hasProperty("hash", is("6769413a79793e242c73d7377f0006c6aea95480"))); + assertThat(instance.fetch(new GitBranchSCMHead("foo"), null), + hasProperty("hash", is("6769413a79793e242c73d7377f0006c6aea95480"))); + assertThat(instance.fetch(new SCMHead("bar"), null), + hasProperty("hash", is("3f0b897057d8b43d3b9ff55e3fdefbb021493470"))); + assertThat(instance.fetch(new SCMHead("manchu"), null), + hasProperty("hash", is("a94782d8d90b56b7e0d277c04589bd2e6f70d2cc"))); + assertThat(instance.fetch(new GitTagSCMHead("v1.0.0", 0L), null), + hasProperty("hash", is("315fd8b5cae3363b29050f1aabfc27c985e22f7e"))); + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetchRevisionByName() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + instance.setOwner(mock(SCMSourceOwner.class)); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + assertThat(instance.fetch("foo", null, null), + hasProperty("hash", is("6769413a79793e242c73d7377f0006c6aea95480"))); + assertThat(instance.fetch("bar", null, null), + hasProperty("hash", is("3f0b897057d8b43d3b9ff55e3fdefbb021493470"))); + assertThat(instance.fetch("manchu", null, null), + hasProperty("hash", is("a94782d8d90b56b7e0d277c04589bd2e6f70d2cc"))); + assertThat(instance.fetch("v1.0.0", null, null), + hasProperty("hash", is("315fd8b5cae3363b29050f1aabfc27c985e22f7e"))); + } + + @Issue("JENKINS-47526") + @Test + public void telescopeFetchActions() throws Exception { + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + assertThat(GitSCMTelescope.of(instance), nullValue()); + AbstractGitSCMSourceTest.ActionableSCMSourceOwner owner = + Mockito.mock(AbstractGitSCMSourceTest.ActionableSCMSourceOwner.class); + when(owner.getSCMSource(instance.getId())).thenReturn(instance); + when(owner.getSCMSources()).thenReturn(Collections.singletonList(instance)); + instance.setOwner(owner); + assertThat(GitSCMTelescope.of(instance), notNullValue()); + + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(), new TagDiscoveryTrait())); + + List actions = instance.fetchActions(null, null); + assertThat(actions, + contains(allOf( + instanceOf(GitRemoteHeadRefAction.class), + hasProperty("remote", is("http://git.test/telescope.git")), + hasProperty("name", is("manchu")) + )) + ); + when(owner.getActions(GitRemoteHeadRefAction.class)) + .thenReturn(Collections.singletonList((GitRemoteHeadRefAction) actions.get(0))); + + assertThat(instance.fetchActions(new SCMHead("foo"), null, null), is(Collections.emptyList())); + assertThat(instance.fetchActions(new SCMHead("bar"), null, null), is(Collections.emptyList())); + assertThat(instance.fetchActions(new SCMHead("manchu"), null, null), contains( + instanceOf(PrimaryInstanceMetadataAction.class))); + assertThat(instance.fetchActions(new GitTagSCMHead("v1.0.0", 0L), null, null), + is(Collections.emptyList())); + } + + @Issue("JENKINS-52754") + @Test + public void gitSCMSourceShouldResolveToolsForMaster() throws Exception { + Assume.assumeTrue("Runs on Unix only", !Launcher.isWindows()); + TaskListener log = StreamTaskListener.fromStdout(); + HelloToolInstaller inst = new HelloToolInstaller("master", "echo Hello", "git"); + GitTool t = new GitTool("myGit", null, Collections.singletonList( + new InstallSourceProperty(Collections.singletonList(inst)))); + t.getDescriptor().setInstallations(t); + + GitTool defaultTool = GitTool.getDefaultInstallation(); + GitTool resolved = (GitTool) defaultTool.translate(jenkins.jenkins, new EnvVars(), TaskListener.NULL); + assertThat(resolved.getGitExe(), org.hamcrest.CoreMatchers.containsString("git")); + + GitSCMSource instance = new GitSCMSource("http://git.test/telescope.git"); + instance.fetchRevisions(log, null); + assertTrue("Installer should be invoked", inst.isInvoked()); + } + + private static class HelloToolInstaller extends CommandInstaller { + + private boolean invoked; + + public HelloToolInstaller(String label, String command, String toolHome) { + super(label, command, toolHome); + } + + public boolean isInvoked() { + return invoked; + } + + @Override + public FilePath performInstallation(ToolInstallation toolInstallation, Node node, TaskListener taskListener) throws IOException, InterruptedException { + taskListener.error("Hello, world!"); + invoked = true; + return super.performInstallation(toolInstallation, node, taskListener); + } + } + + @TestExtension + public static class MyGitSCMTelescope extends GitSCMTelescope { + @Override + public boolean supports(@NonNull String remote) { + return "http://git.test/telescope.git".equals(remote); + } + + @Override + public boolean supportsDescriptor(SCMDescriptor descriptor) { + return false; + } + + @Override + public boolean supportsDescriptor(SCMSourceDescriptor descriptor) { + return false; + } + + @Override + public void validate(@NonNull String remote, StandardCredentials credentials) + throws IOException, InterruptedException { + } + + @Override + protected SCMFileSystem build(@NonNull String remote, StandardCredentials credentials, + @NonNull SCMHead head, + final SCMRevision rev) throws IOException, InterruptedException { + final String hash; + if (rev instanceof AbstractGitSCMSource.SCMRevisionImpl) { + hash = ((AbstractGitSCMSource.SCMRevisionImpl) rev).getHash(); + } else { + switch (head.getName()) { + case "foo": + hash = "6769413a79793e242c73d7377f0006c6aea95480"; + break; + case "bar": + hash = "3f0b897057d8b43d3b9ff55e3fdefbb021493470"; + break; + case "manchu": + hash = "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc"; + break; + case "v1.0.0": + hash = "315fd8b5cae3363b29050f1aabfc27c985e22f7e"; + break; + default: + return null; + } + } + return new SCMFileSystem(rev) { + @Override + public long lastModified() throws IOException, InterruptedException { + switch (hash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + return 15086163840000L; + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + return 15086173840000L; + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + return 15086183840000L; + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + return 15086193840000L; + } + return 0L; + } + + @NonNull + @Override + public SCMFile getRoot() { + return new MySCMFile(hash); + } + }; + } + + @Override + public long getTimestamp(@NonNull String remote, StandardCredentials credentials, @NonNull String refOrHash) + throws IOException, InterruptedException { + switch (refOrHash) { + case "refs/heads/foo": + refOrHash = "6769413a79793e242c73d7377f0006c6aea95480"; + break; + case "refs/heads/bar": + refOrHash = "3f0b897057d8b43d3b9ff55e3fdefbb021493470"; + break; + case "refs/heads/manchu": + refOrHash = "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc"; + break; + case "refs/tags/v1.0.0": + refOrHash = "315fd8b5cae3363b29050f1aabfc27c985e22f7e"; + break; + } + switch (refOrHash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + return 15086163840000L; + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + return 15086173840000L; + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + return 15086183840000L; + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + return 15086193840000L; + } + return 0L; + } + + @Override + public SCMRevision getRevision(@NonNull String remote, StandardCredentials credentials, + @NonNull String refOrHash) + throws IOException, InterruptedException { + switch (refOrHash) { + case "refs/heads/foo": + return new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ); + case "refs/heads/bar": + return new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ); + case "refs/heads/manchu": + return new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("manchu"), "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc" + ); + case "refs/tags/v1.0.0": + return new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), + "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ); + } + return null; + } + + @Override + public Iterable getRevisions(@NonNull String remote, StandardCredentials credentials, + @NonNull Set referenceTypes) + throws IOException, InterruptedException { + return Arrays.asList( + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("foo"), "6769413a79793e242c73d7377f0006c6aea95480" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("bar"), "3f0b897057d8b43d3b9ff55e3fdefbb021493470" + ), + new AbstractGitSCMSource.SCMRevisionImpl( + new SCMHead("manchu"), "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc" + ), + new GitTagSCMRevision( + new GitTagSCMHead("v1.0.0", 15086193840000L), "315fd8b5cae3363b29050f1aabfc27c985e22f7e" + ) + ); + } + + @Override + public String getDefaultTarget(@NonNull String remote, StandardCredentials credentials) + throws IOException, InterruptedException { + return "manchu"; + } + + private static class MySCMFile extends SCMFile { + private final String hash; + private final SCMFile.Type type; + + public MySCMFile(String hash) { + this.hash = hash; + this.type = Type.DIRECTORY; + } + + public MySCMFile(MySCMFile parent, String name, SCMFile.Type type) { + super(parent, name); + this.type = type; + this.hash = parent.hash; + } + + @NonNull + @Override + protected SCMFile newChild(@NonNull String name, boolean assumeIsDirectory) { + return new MySCMFile(this, name, assumeIsDirectory ? Type.DIRECTORY : Type.REGULAR_FILE); + } + + @NonNull + @Override + public Iterable children() throws IOException, InterruptedException { + if (parent().isRoot()) { + switch (hash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + return Collections.singleton(newChild("Jenkinsfile", false)); + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + return Arrays.asList(newChild("Jenkinsfile", false), + newChild("README.md", false)); + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + return Collections.singleton(newChild("README.md", false)); + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + return Collections.singleton(newChild("Jenkinsfile", false)); + } + } + return Collections.emptySet(); + } + + @Override + public long lastModified() throws IOException, InterruptedException { + switch (hash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + return 15086163840000L; + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + return 15086173840000L; + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + return 15086183840000L; + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + return 15086193840000L; + } + return 0L; + } + + @NonNull + @Override + protected Type type() throws IOException, InterruptedException { + switch (hash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + switch (getPath()) { + case "Jenkinsfile": + return Type.REGULAR_FILE; + } + break; + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + switch (getPath()) { + case "Jenkinsfile": + return Type.REGULAR_FILE; + case "README.md": + return Type.REGULAR_FILE; + } + break; + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + switch (getPath()) { + case "README.md": + return Type.REGULAR_FILE; + } + break; + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + switch (getPath()) { + case "Jenkinsfile": + return Type.REGULAR_FILE; + } + break; + } + return type == Type.DIRECTORY ? type : Type.NONEXISTENT; + } + + @NonNull + @Override + public InputStream content() throws IOException, InterruptedException { + switch (hash) { + case "6769413a79793e242c73d7377f0006c6aea95480": + switch (getPath()) { + case "Jenkinsfile": + return new ByteArrayInputStream("pipeline{}".getBytes(StandardCharsets.UTF_8)); + } + break; + case "3f0b897057d8b43d3b9ff55e3fdefbb021493470": + switch (getPath()) { + case "Jenkinsfile": + return new ByteArrayInputStream("pipeline{}".getBytes(StandardCharsets.UTF_8)); + case "README.md": + return new ByteArrayInputStream("Welcome".getBytes(StandardCharsets.UTF_8)); + } + break; + case "a94782d8d90b56b7e0d277c04589bd2e6f70d2cc": + switch (getPath()) { + case "README.md": + return new ByteArrayInputStream("Welcome".getBytes(StandardCharsets.UTF_8)); + } + break; + case "315fd8b5cae3363b29050f1aabfc27c985e22f7e": + switch (getPath()) { + case "Jenkinsfile": + return new ByteArrayInputStream("pipeline{}".getBytes(StandardCharsets.UTF_8)); + } + break; + } + throw new FileNotFoundException(getPath() + " does not exist"); + } + } + } + + private static class MySCMSourceCriteria implements SCMSourceCriteria { + + private final String path; + + private MySCMSourceCriteria(String path) { + this.path = path; + } + + @Override + public boolean isHead(@NonNull Probe probe, @NonNull TaskListener listener) throws IOException { + return SCMFile.Type.REGULAR_FILE.equals(probe.stat(path).getType()); + } + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMSourceTraitsTest.java b/src/test/java/jenkins/plugins/git/GitSCMSourceTraitsTest.java new file mode 100644 index 0000000000..1885ed521e --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMSourceTraitsTest.java @@ -0,0 +1,382 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.AuthorInChangelog; +import hudson.plugins.git.extensions.impl.CheckoutOption; +import hudson.plugins.git.extensions.impl.CleanBeforeCheckout; +import hudson.plugins.git.extensions.impl.CleanCheckout; +import hudson.plugins.git.extensions.impl.CloneOption; +import hudson.plugins.git.extensions.impl.GitLFSPull; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.plugins.git.extensions.impl.PruneStaleBranch; +import hudson.plugins.git.extensions.impl.SparseCheckoutPaths; +import hudson.plugins.git.extensions.impl.SubmoduleOption; +import hudson.plugins.git.extensions.impl.UserIdentity; +import hudson.plugins.git.extensions.impl.WipeWorkspace; + +import java.util.Collections; +import jenkins.model.Jenkins; + +import jenkins.plugins.git.traits.AuthorInChangelogTrait; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.plugins.git.traits.CheckoutOptionTrait; +import jenkins.plugins.git.traits.CleanAfterCheckoutTrait; +import jenkins.plugins.git.traits.CleanBeforeCheckoutTrait; +import jenkins.plugins.git.traits.CloneOptionTrait; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; +import jenkins.plugins.git.traits.GitLFSPullTrait; +import jenkins.plugins.git.traits.IgnoreOnPushNotificationTrait; +import jenkins.plugins.git.traits.LocalBranchTrait; +import jenkins.plugins.git.traits.PruneStaleBranchTrait; +import jenkins.plugins.git.traits.RefSpecsSCMSourceTrait; +import jenkins.plugins.git.traits.RemoteNameSCMSourceTrait; +import jenkins.plugins.git.traits.SparseCheckoutPathsTrait; +import jenkins.plugins.git.traits.SubmoduleOptionTrait; +import jenkins.plugins.git.traits.UserIdentityTrait; +import jenkins.plugins.git.traits.WipeWorkspaceTrait; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import org.hamcrest.Matchers; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class GitSCMSourceTraitsTest { + /** + * All tests in this class only use Jenkins for the extensions + */ + @ClassRule + public static JenkinsRule r = new JenkinsRule(); + + @Rule + public TestName currentTestName = new TestName(); + + private GitSCMSource load() { + return load(currentTestName.getMethodName()); + } + + private GitSCMSource load(String dataSet) { + return (GitSCMSource) Jenkins.XSTREAM2.fromXML( + getClass().getResource(getClass().getSimpleName() + "/" + dataSet + ".xml")); + } + + @Test + public void modern() throws Exception { + GitSCMSource instance = load(); + assertThat(instance.getId(), is("5b061c87-da5c-4d69-b9d5-b041d065c945")); + assertThat(instance.getRemote(), is("git://git.test/example.git")); + assertThat(instance.getCredentialsId(), is(nullValue())); + assertThat(instance.getTraits(), is(Collections.emptyList())); + } + + @Test + public void cleancheckout_v1_extension() { + verifyCleanCheckoutTraits(false); + } + + @Test + public void cleancheckout_v1_trait() { + verifyCleanCheckoutTraits(false); + } + + @Test + public void cleancheckout_v2_extension() { + verifyCleanCheckoutTraits(true); + } + + @Test + public void cleancheckout_v2_trait() { + verifyCleanCheckoutTraits(true); + } + + /** + * Tests loading of {@link CleanCheckout}/{@link CleanBeforeCheckout}. + */ + private void verifyCleanCheckoutTraits(boolean deleteUntrackedNestedRepositories) { + GitSCMSource instance = load(); + + assertThat(instance.getTraits(), + hasItems( + allOf( + instanceOf(CleanBeforeCheckoutTrait.class), + hasProperty("extension", + hasProperty( + "deleteUntrackedNestedRepositories", + is(deleteUntrackedNestedRepositories) + ) + ) + ), + allOf( + instanceOf(CleanAfterCheckoutTrait.class), + hasProperty("extension", + hasProperty( + "deleteUntrackedNestedRepositories", + is(deleteUntrackedNestedRepositories) + ) + ) + ) + ) + ); + } + + @Test + public void pimpped_out() throws Exception { + GitSCMSource instance = load(); + assertThat(instance.getId(), is("fd2380f8-d34f-48d5-8006-c34542bc4a89")); + assertThat(instance.getRemote(), is("git://git.test/example.git")); + assertThat(instance.getCredentialsId(), is("e4d8c11a-0d24-472f-b86b-4b017c160e9a")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.instanceOf(BranchDiscoveryTrait.class), + Matchers.allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("foo/*")), + hasProperty("excludes", is("bar/*")) + ), + Matchers.allOf( + instanceOf(CheckoutOptionTrait.class), + hasProperty("extension", + hasProperty("timeout", is(5)) + ) + ), + Matchers.allOf( + instanceOf(CloneOptionTrait.class), + hasProperty("extension", + allOf( + hasProperty("shallow", is(true)), + hasProperty("noTags", is(true)), + hasProperty("reference", is("origin/foo")), + hasProperty("timeout", is(3)), + hasProperty("depth", is(3)) + ) + ) + ), + Matchers.allOf( + instanceOf(SubmoduleOptionTrait.class), + hasProperty("extension", + allOf( + hasProperty("disableSubmodules", is(true)), + hasProperty("recursiveSubmodules", is(true)), + hasProperty("trackingSubmodules", is(true)), + hasProperty("reference", is("origin/bar")), + hasProperty("parentCredentials", is(true)), + hasProperty("timeout", is(4)), + hasProperty("shallow", is(true)), + hasProperty("depth", is(3)), + hasProperty("threads", is(4)) + ) + ) + ), + Matchers.allOf( + instanceOf(LocalBranchTrait.class), + hasProperty("extension", + hasProperty("localBranch", is("**")) + ) + ), + Matchers.allOf( + instanceOf(CleanBeforeCheckoutTrait.class), + hasProperty("extension", + hasProperty("deleteUntrackedNestedRepositories", is(true)) + ) + ), + Matchers.allOf( + instanceOf(CleanAfterCheckoutTrait.class), + hasProperty("extension", + hasProperty("deleteUntrackedNestedRepositories", is(true)) + ) + ), + Matchers.allOf( + instanceOf(UserIdentityTrait.class), + hasProperty("extension", + allOf( + hasProperty("name", is("bob")), + hasProperty("email", is("bob@example.com")) + ) + ) + ), + Matchers.instanceOf(GitLFSPullTrait.class), + Matchers.instanceOf(PruneStaleBranchTrait.class), + Matchers.instanceOf(IgnoreOnPushNotificationTrait.class), + Matchers.instanceOf(AuthorInChangelogTrait.class), + Matchers.instanceOf(WipeWorkspaceTrait.class), + Matchers.allOf( + instanceOf(GitBrowserSCMSourceTrait.class), + hasProperty("browser", + allOf( + instanceOf(BitbucketWeb.class), + hasProperty("repoUrl", is("foo")) + ) + ) + ), + Matchers.allOf( + instanceOf(SparseCheckoutPathsTrait.class), + hasProperty("extension", + allOf( + hasProperty("sparseCheckoutPaths", hasSize(2)) + ) + ) + ) + ) + ); + // Legacy API + assertThat(instance.getIncludes(), is("foo/*")); + assertThat(instance.getExcludes(), is("bar/*")); + assertThat( + "We have trimmed the extension to only those that are supported on GitSCMSource", + instance.getExtensions(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(CheckoutOption.class), + hasProperty("timeout", is(5)) + ), + Matchers.allOf( + instanceOf(CloneOption.class), + hasProperty("shallow", is(true)), + hasProperty("noTags", is(true)), + hasProperty("reference", is("origin/foo")), + hasProperty("timeout", is(3)), + hasProperty("depth", is(3)) + ), + Matchers.allOf( + instanceOf(SubmoduleOption.class), + hasProperty("disableSubmodules", is(true)), + hasProperty("recursiveSubmodules", is(true)), + hasProperty("trackingSubmodules", is(true)), + hasProperty("reference", is("origin/bar")), + hasProperty("parentCredentials", is(true)), + hasProperty("timeout", is(4)) + ), + Matchers.allOf( + instanceOf(LocalBranch.class), + hasProperty("localBranch", is("**")) + ), + Matchers.instanceOf(CleanCheckout.class), + Matchers.instanceOf(CleanBeforeCheckout.class), + Matchers.allOf( + instanceOf(UserIdentity.class), + hasProperty("name", is("bob")), + hasProperty("email", is("bob@example.com")) + ), + Matchers.instanceOf(GitLFSPull.class), + Matchers.instanceOf(PruneStaleBranch.class), + Matchers.instanceOf(AuthorInChangelog.class), + Matchers.instanceOf(WipeWorkspace.class), + Matchers.allOf( + instanceOf(SparseCheckoutPaths.class), + hasProperty("sparseCheckoutPaths", hasSize(2)) + ) + ) + ); + assertThat(instance.getBrowser(), allOf( + instanceOf(BitbucketWeb.class), + hasProperty("repoUrl", is("foo")) + )); + assertThat(instance.isIgnoreOnPushNotifications(), is(true)); + assertThat(instance.getRemoteName(), is("origin")); + assertThat(instance.getRawRefSpecs(), is("+refs/heads/*:refs/remotes/origin/*")); + } + + @Test + public void given__modernCode__when__constructor__then__traitsEmpty() throws Exception { + assertThat(new GitSCMSource("git://git.test/example.git").getTraits(), is(empty())); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults1() throws Exception { + GitSCMSource instance = new GitSCMSource("id", "git://git.test/example.git", null, "*", "", false); + assertThat(instance.getTraits(), contains( + instanceOf(BranchDiscoveryTrait.class) + )); + assertThat(instance.isIgnoreOnPushNotifications(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getRemoteName(), is("origin")); + assertThat(instance.getRawRefSpecs(), is("+refs/heads/*:refs/remotes/origin/*")); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults2() throws Exception { + GitSCMSource instance = new GitSCMSource("id", "git://git.test/example.git", null, "*", "", true); + assertThat(instance.getTraits(), containsInAnyOrder( + instanceOf(BranchDiscoveryTrait.class), + instanceOf(IgnoreOnPushNotificationTrait.class) + )); + assertThat(instance.isIgnoreOnPushNotifications(), is(true)); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults3() throws Exception { + GitSCMSource instance = new GitSCMSource("id", "git://git.test/example.git", null, "foo/*", "", false); + assertThat(instance.getTraits(), contains( + instanceOf(BranchDiscoveryTrait.class), + allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("foo/*")), + hasProperty("excludes", is("")) + ) + )); + assertThat(instance.getIncludes(), is("foo/*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults4() throws Exception { + GitSCMSource instance = new GitSCMSource("id", "git://git.test/example.git", null, "", "foo/*", false); + assertThat(instance.getTraits(), contains( + instanceOf(BranchDiscoveryTrait.class), + allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("foo/*")) + ) + )); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("foo/*")); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults5() throws Exception { + GitSCMSource instance = + new GitSCMSource("id", "git://git.test/example.git", null, "upstream", null, "*", "", false); + assertThat(instance.getTraits(), contains( + instanceOf(BranchDiscoveryTrait.class), + allOf( + instanceOf(RemoteNameSCMSourceTrait.class), + hasProperty("remoteName", is("upstream")) + ) + )); + assertThat(instance.getRemoteName(), is("upstream")); + } + + @Test + public void given__legacyCode__when__constructor__then__traitsContainLegacyDefaults6() throws Exception { + GitSCMSource instance = + new GitSCMSource("id", "git://git.test/example.git", null, null, "refs/pulls/*:refs/upstream/*", "*", + "", false); + assertThat(instance.getTraits(), contains( + instanceOf(BranchDiscoveryTrait.class), + allOf( + instanceOf(RefSpecsSCMSourceTrait.class), + hasProperty("templates", contains(hasProperty("value", is("refs/pulls/*:refs/upstream/*")))) + ) + )); + assertThat(instance.getRawRefSpecs(), is("refs/pulls/*:refs/upstream/*")); + } + + +} diff --git a/src/test/java/jenkins/plugins/git/GitSCMTelescopeTest.java b/src/test/java/jenkins/plugins/git/GitSCMTelescopeTest.java new file mode 100644 index 0000000000..55f82c19b8 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSCMTelescopeTest.java @@ -0,0 +1,731 @@ +/* + * The MIT License + * + * Copyright 2017 Mark Waite. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.model.Job; +import hudson.model.TaskListener; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import hudson.plugins.git.browser.GitWeb; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.scm.NullSCM; +import hudson.scm.SCM; +import hudson.scm.SCMDescriptor; +import hudson.search.Search; +import hudson.search.SearchIndex; +import hudson.security.ACL; +import hudson.security.Permission; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; +import jenkins.plugins.git.traits.GitToolSCMSourceTrait; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.SCMSourceDescriptor; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.acegisecurity.AccessDeniedException; +import org.junit.Test; +import static org.hamcrest.Matchers.*; +import org.jenkinsci.plugins.gitclient.GitClient; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.ClassRule; + +public class GitSCMTelescopeTest /* extends AbstractGitRepository */ { + + private final StandardCredentials credentials = null; + + /* REPO can be allocated once for the whole test suite so long as nothing changes it */ + @ClassRule + static public final GitSampleRepoRule READ_ONLY_REPO = new GitSampleRepoRule(); + + private final String remote; + private GitSCMTelescope telescope; + + public GitSCMTelescopeTest() { + remote = READ_ONLY_REPO.fileUrl(); + } + + @Before + public void createTelescopeForRemote() { + telescope = new GitSCMTelescopeImpl(remote); + } + + @Test + public void testOf_GitSCM() { + /* Testing GitSCMTelescope.of() for non null return needs JenkinsRule */ + GitSCM multiBranchSource = new GitSCM(remote); + GitSCMTelescope telescopeOfMultiBranchSource = GitSCMTelescope.of(multiBranchSource); + assertThat(telescopeOfMultiBranchSource, is(nullValue())); + } + + @Test + public void testOf_AbstractGitSCMSource() { + AbstractGitSCMSource source = new AbstractGitSCMSourceImpl(); + GitSCMTelescope telescopeOfAbstractGitSCMSource = GitSCMTelescope.of(source); + assertThat(telescopeOfAbstractGitSCMSource, is(nullValue())); + } + + @Test + public void testSupports_StringFalse() { + GitSCMTelescope telescopeWithoutRemote = new GitSCMTelescopeImpl(); + assertFalse(telescopeWithoutRemote.supports(remote)); + } + + @Test + public void testSupports_String() { + assertTrue(telescope.supports(remote)); + } + + @Test + public void testValidate() throws Exception { + telescope.validate(remote, credentials); + } + + /** + * Return a GitSCM defined with a branchSpecList which exactly matches a + * single branch. GitSCMTelescope requires a GitSCM that matches a single + * branch, with no wildcards in the branch name. + * + * @param repoUrl URL to the repository for the returned GitSCM + * @return GitSCM with a single branch in its definition + */ + private GitSCM getSingleBranchSource(String repoUrl) { + UserRemoteConfig remoteConfig = new UserRemoteConfig( + repoUrl, + "origin", + "+refs/heads/master:refs/remotes/origin/master", + null); + List remoteConfigList = new ArrayList<>(); + remoteConfigList.add(remoteConfig); + BranchSpec masterBranchSpec = new BranchSpec("master"); + List branchSpecList = new ArrayList<>(); + branchSpecList.add(masterBranchSpec); + boolean doGenerateSubmoduleConfigurations = false; + Collection submoduleCfg = new ArrayList<>(); + GitRepositoryBrowser browser = new GitWeb(repoUrl); + String gitTool = "Default"; + List extensions = null; + GitSCM singleBranchSource = new GitSCM(remoteConfigList, + branchSpecList, + doGenerateSubmoduleConfigurations, + submoduleCfg, + browser, + gitTool, + extensions); + return singleBranchSource; + } + + @Test + public void testSupports_SCM() throws Exception { + GitSCM singleBranchSource = getSingleBranchSource(remote); + // single branch source is supported by telescope + assertTrue(telescope.supports(singleBranchSource)); + } + + @Test + public void testSupports_SCMNullSCM() throws Exception { + NullSCM nullSCM = new NullSCM(); + // NullSCM is not supported by telescope + assertFalse(telescope.supports(nullSCM)); + } + + @Test + public void testSupports_SCMMultiBranchSource() throws Exception { + GitSCM multiBranchSource = new GitSCM(remote); + // Multi-branch source is not supported by telescope + assertFalse(telescope.supports(multiBranchSource)); + } + + @Test + public void testSupports_SCMSource() { + SCMSource source = new GitSCMSource(remote); + SCMSourceOwner sourceOwner = new SCMSourceOwnerImpl(); + source.setOwner(sourceOwner); + assertTrue(telescope.supports(source)); + } + + @Test + public void testSupports_SCMSourceNoOwner() { + SCMSource source = new GitSCMSource(remote); + // SCMSource without an owner not supported by telescope + assertFalse(telescope.supports(source)); + } + + @Test + public void testSupports_SCMSourceNullSource() { + SCMSource source = new SCMSourceImpl(); + // Non AbstractGitSCMSource is not supported by telescope + assertFalse(telescope.supports(source)); + } + + @Test + public void testGetTimestamp_3args_1() throws Exception { + String refOrHash = "master"; + assertThat(telescope.getTimestamp(remote, credentials, refOrHash), is(12345L)); + } + + @Test + public void testGetTimestamp_3args_2Tag() throws Exception { + SCMHead head = new GitTagSCMHead("git-tag-name", 56789L); + assertThat(telescope.getTimestamp(remote, credentials, head), is(12345L)); + } + + @Test + public void testGetTimestamp_3args_2() throws Exception { + SCMHead head = new SCMHead("git-tag-name"); + assertThat(telescope.getTimestamp(remote, credentials, head), is(12345L)); + } + + @Test + public void testGetDefaultTarget() throws Exception { + assertThat(telescope.getDefaultTarget(remote, null), is("")); + } + + @Test + public void testBuild_3args_1() throws Exception { + SCMSource source = new GitSCMSource(remote); + SCMSourceOwner sourceOwner = new SCMSourceOwnerImpl(); + source.setOwner(sourceOwner); + SCMHead head = new SCMHead("some-name"); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + SCMFileSystem fileSystem = telescope.build(source, head, rev); + assertThat(fileSystem.getRevision(), is(rev)); + assertThat(fileSystem.isFixedRevision(), is(true)); + } + + @Test + public void testBuild_3args_1NoOwner() throws Exception { + SCMSource source = new GitSCMSource(remote); + SCMHead head = new SCMHead("some-name"); + SCMRevision rev = null; + // When source has no owner, build returns null + assertThat(telescope.build(source, head, rev), is(nullValue())); + } + + @Test + public void testBuild_3args_2() throws Exception { + Item owner = new ItemImpl(); + SCM scm = getSingleBranchSource(remote); + SCMHead head = new SCMHead("some-name"); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + SCMFileSystem fileSystem = telescope.build(owner, scm, rev); + assertThat(fileSystem.getRevision(), is(rev)); + assertThat(fileSystem.isFixedRevision(), is(true)); + } + + @Test + public void testBuild_4args() throws Exception { + SCMHead head = new SCMHead("some-name"); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + SCMFileSystem fileSystem = telescope.build(remote, credentials, head, rev); + assertThat(fileSystem, is(notNullValue())); + assertThat(fileSystem.getRevision(), is(rev)); + assertThat(fileSystem.isFixedRevision(), is(true)); + } + + @Test + public void testGetRevision_3args_1() throws Exception { + SCMHead head = new SCMHead("some-name"); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + GitSCMTelescope telescopeWithRev = new GitSCMTelescopeImpl(remote, rev); + String refOrHash = "master"; + assertThat(telescopeWithRev.getRevision(remote, credentials, refOrHash), is(rev)); + } + + @Test + public void testGetRevision_3args_2() throws Exception { + SCMHead head = new GitTagSCMHead("git-tag-name", 56789L); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + GitSCMTelescope telescopeWithRev = new GitSCMTelescopeImpl(remote, rev); + assertThat(telescopeWithRev.getRevision(remote, credentials, head), is(rev)); + } + + @Test + public void testGetRevisions_3args() throws Exception { + Set referenceTypes = new HashSet<>(); + Iterable revisions = telescope.getRevisions(remote, credentials, referenceTypes); + assertThat(revisions, is(notNullValue())); + assertFalse(revisions.iterator().hasNext()); + } + + @Test + public void testGetRevisions_3argsWithRev() throws Exception { + Set referenceTypes = new HashSet<>(); + SCMHead head = new GitTagSCMHead("git-tag-name", 56789L); + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + GitSCMTelescope telescopeWithRev = new GitSCMTelescopeImpl(remote, rev); + Iterable revisions = telescopeWithRev.getRevisions(remote, credentials, referenceTypes); + assertThat(revisions, is(notNullValue())); + Iterator revIterator = revisions.iterator(); + assertThat(revIterator.next(), is(rev)); + assertFalse(revIterator.hasNext()); + } + + @Test + public void testGetRevisions_String_StandardCredentials() throws Exception { + String SHA1 = "0123456789abcdef0123456789abcdef01234567"; + SCMHead head = new GitTagSCMHead("git-tag-name", 56789L); + SCMRevision rev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + GitSCMTelescope telescopeWithRev = new GitSCMTelescopeImpl(remote, rev); + Iterable revisions = telescopeWithRev.getRevisions(remote, credentials); + assertThat(revisions, is(notNullValue())); + Iterator revIterator = revisions.iterator(); + assertThat(revIterator.next(), is(rev)); + assertFalse(revIterator.hasNext()); + } + + /* ********************* Test helper classes **************************** */ + private static class ItemImpl implements Item { + + public ItemImpl() { + } + + @Override + public ItemGroup getParent() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public Collection getAllJobs() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getFullName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getDisplayName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getFullDisplayName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getRelativeNameFrom(ItemGroup ig) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getRelativeNameFrom(Item item) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getShortUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getAbsoluteUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onLoad(ItemGroup ig, String string) throws IOException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onCopiedFrom(Item item) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onCreatedFromScratch() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void save() throws IOException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void delete() throws IOException, InterruptedException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public File getRootDir() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public Search getSearch() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getSearchName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getSearchUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public SearchIndex getSearchIndex() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public ACL getACL() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void checkPermission(Permission prmsn) throws AccessDeniedException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public boolean hasPermission(Permission prmsn) { + throw new UnsupportedOperationException("Not called."); + } + } + + private static class SCMSourceImpl extends SCMSource { + + public SCMSourceImpl() { + } + + @Override + protected void retrieve(SCMSourceCriteria scmsc, SCMHeadObserver scmho, SCMHeadEvent scmhe, TaskListener tl) throws IOException, InterruptedException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public SCM build(SCMHead scmh, SCMRevision scmr) { + throw new UnsupportedOperationException("Not called."); + } + } + + private static class GitSCMTelescopeImpl extends GitSCMTelescope { + + final private String allowedRemote; + final private SCMRevision revision; + private List revisionList = new ArrayList<>(); + + public GitSCMTelescopeImpl(String allowedRemote, SCMRevision revision) { + this.allowedRemote = allowedRemote; + this.revision = revision; + revisionList = new ArrayList<>(); + revisionList.add(this.revision); + } + + public GitSCMTelescopeImpl(String allowedRemote) { + this.allowedRemote = allowedRemote; + this.revision = null; + revisionList = new ArrayList<>(); + } + + public GitSCMTelescopeImpl() { + this.allowedRemote = null; + this.revision = null; + revisionList = new ArrayList<>(); + } + + @Override + public boolean supports(String remote) { + if (allowedRemote == null) { + return false; + } + return allowedRemote.equals(remote); + } + + @Override + public boolean supportsDescriptor(SCMDescriptor descriptor) { + return false; + } + + @Override + public boolean supportsDescriptor(SCMSourceDescriptor descriptor) { + return false; + } + + @Override + public void validate(String remote, StandardCredentials credentials) throws IOException, InterruptedException { + } + + @Override + public SCMFileSystem build(String remote, StandardCredentials credentials, SCMHead head, SCMRevision rev) throws IOException, InterruptedException { + GitClient client = null; + AbstractGitSCMSource.SCMRevisionImpl myRev = null; + if (rev != null) { + String SHA1 = rev.toString(); + myRev = new AbstractGitSCMSource.SCMRevisionImpl(head, SHA1); + } + return new GitSCMFileSystem(client, remote, head.toString(), myRev); + } + + @Override + public long getTimestamp(String remote, StandardCredentials credentials, String refOrHash) throws IOException, InterruptedException { + return 12345L; + } + + @Override + public SCMRevision getRevision(String remote, StandardCredentials credentials, String refOrHash) throws IOException, InterruptedException { + return revision; + } + + @Override + public Iterable getRevisions(String remote, StandardCredentials credentials, Set referenceTypes) throws IOException, InterruptedException { + return revisionList; + } + + @Override + public String getDefaultTarget(String remote, StandardCredentials credentials) throws IOException, InterruptedException { + return ""; + } + } + + private static class AbstractGitSCMSourceImpl extends AbstractGitSCMSource { + + public AbstractGitSCMSourceImpl() { + setId("AbstractGitSCMSourceImpl-id"); + } + + @NonNull + @Override + public List getTraits() { + return Collections.singletonList(new GitToolSCMSourceTrait("git-custom") { + @Override + public SCMSourceTraitDescriptor getDescriptor() { + return new GitBrowserSCMSourceTrait.DescriptorImpl(); + } + }); + } + + @Override + public String getCredentialsId() { + return ""; + } + + @Override + public String getRemote() { + return ""; + } + + @Override + public SCMSourceDescriptor getDescriptor() { + return new DescriptorImpl(); + } + + public static class DescriptorImpl extends SCMSourceDescriptor { + + @Override + public String getDisplayName() { + return null; + } + } + } + + private static class SCMSourceOwnerImpl implements SCMSourceOwner { + + @Override + public List getSCMSources() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public SCMSource getSCMSource(String string) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onSCMSourceUpdated(SCMSource scms) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public SCMSourceCriteria getSCMSourceCriteria(SCMSource scms) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public ItemGroup getParent() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public Collection getAllJobs() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getFullName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getDisplayName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getFullDisplayName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getRelativeNameFrom(ItemGroup ig) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getRelativeNameFrom(Item item) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getShortUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getAbsoluteUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onLoad(ItemGroup ig, String string) throws IOException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onCopiedFrom(Item item) { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void onCreatedFromScratch() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void save() throws IOException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void delete() throws IOException, InterruptedException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public File getRootDir() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public Search getSearch() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getSearchName() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public String getSearchUrl() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public SearchIndex getSearchIndex() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public ACL getACL() { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public void checkPermission(Permission prmsn) throws AccessDeniedException { + throw new UnsupportedOperationException("Not called."); + } + + @Override + public boolean hasPermission(Permission prmsn) { + throw new UnsupportedOperationException("Not called."); + } + } +} diff --git a/src/test/java/jenkins/plugins/git/GitSampleRepoRule.java b/src/test/java/jenkins/plugins/git/GitSampleRepoRule.java new file mode 100644 index 0000000000..c88192c129 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitSampleRepoRule.java @@ -0,0 +1,132 @@ +/* + * The MIT License + * + * Copyright 2015 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.plugins.git; + +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.util.NameValuePair; +import hudson.Launcher; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.scm.impl.mock.AbstractSampleDVCSRepoRule; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.RepositoryBuilder; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Manages a sample Git repository. + */ +public final class GitSampleRepoRule extends AbstractSampleDVCSRepoRule { + + private static boolean initialized = false; + + private static final Logger LOGGER = Logger.getLogger(GitSampleRepoRule.class.getName()); + + public void git(String... cmds) throws Exception { + run("git", cmds); + } + + private static void checkGlobalConfig() throws Exception { + if (initialized) return; + initialized = true; + CliGitCommand gitCmd = new CliGitCommand(null); + gitCmd.setDefaults(); + } + + @Override + public void init() throws Exception { + run(true, tmp.getRoot(), "git", "version"); + checkGlobalConfig(); + git("init"); + write("file", ""); + git("add", "file"); + git("config", "user.name", "Git SampleRepoRule"); + git("config", "user.email", "gits@mplereporule"); + git("commit", "--message=init"); + } + + public final boolean mkdirs(String rel) throws IOException { + return new File(this.sampleRepo, rel).mkdirs(); + } + + public void notifyCommit(JenkinsRule r) throws Exception { + synchronousPolling(r); + WebResponse webResponse = r.createWebClient().goTo("git/notifyCommit?url=" + bareUrl(), "text/plain").getWebResponse(); + LOGGER.log(Level.FINE, webResponse.getContentAsString()); + for (NameValuePair pair : webResponse.getResponseHeaders()) { + if (pair.getName().equals("Triggered")) { + LOGGER.log(Level.FINE, "Triggered: " + pair.getValue()); + } + } + r.waitUntilNoActivity(); + } + + public String head() throws Exception { + return new RepositoryBuilder().setWorkTree(sampleRepo).build().resolve(Constants.HEAD).name(); + } + + public File getRoot() { + return this.sampleRepo; + } + + public boolean gitVersionAtLeast(int neededMajor, int neededMinor) { + return gitVersionAtLeast(neededMajor, neededMinor, 0); + } + + public boolean gitVersionAtLeast(int neededMajor, int neededMinor, int neededPatch) { + final TaskListener procListener = StreamTaskListener.fromStderr(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + int returnCode = new Launcher.LocalLauncher(procListener).launch().cmds("git", "--version").stdout(out).join(); + if (returnCode != 0) { + LOGGER.log(Level.WARNING, "Command 'git --version' returned " + returnCode); + } + } catch (IOException | InterruptedException ex) { + LOGGER.log(Level.WARNING, "Exception checking git version " + ex); + } + final String versionOutput = out.toString().trim(); + final String[] fields = versionOutput.split(" ")[2].replaceAll("msysgit.", "").replaceAll("windows.", "").split("\\."); + final int gitMajor = Integer.parseInt(fields[0]); + final int gitMinor = Integer.parseInt(fields[1]); + final int gitPatch = Integer.parseInt(fields[2]); + if (gitMajor < 1 || gitMajor > 3) { + LOGGER.log(Level.WARNING, "Unexpected git major version " + gitMajor + " parsed from '" + versionOutput + "', field:'" + fields[0] + "'"); + } + if (gitMinor < 0 || gitMinor > 50) { + LOGGER.log(Level.WARNING, "Unexpected git minor version " + gitMinor + " parsed from '" + versionOutput + "', field:'" + fields[1] + "'"); + } + if (gitPatch < 0 || gitPatch > 20) { + LOGGER.log(Level.WARNING, "Unexpected git patch version " + gitPatch + " parsed from '" + versionOutput + "', field:'" + fields[2] + "'"); + } + + return gitMajor > neededMajor || + (gitMajor == neededMajor && gitMinor > neededMinor) || + (gitMajor == neededMajor && gitMinor == neededMinor && gitPatch >= neededPatch); + } +} diff --git a/src/test/java/jenkins/plugins/git/GitStepTest.java b/src/test/java/jenkins/plugins/git/GitStepTest.java new file mode 100644 index 0000000000..fdbbac0fbd --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitStepTest.java @@ -0,0 +1,260 @@ +/* + * The MIT License + * + * Copyright 2014 Jesse Glick. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.plugins.git; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import hudson.model.Label; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.GitTagAction; +import hudson.plugins.git.util.BuildData; +import hudson.scm.ChangeLogSet; +import hudson.scm.SCM; +import hudson.triggers.SCMTrigger; +import java.util.Iterator; +import java.util.List; +import jenkins.util.VirtualFile; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * @author Nicolas De Loof + */ +public class GitStepTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + @Rule + public GitSampleRepoRule otherRepo = new GitSampleRepoRule(); + + @BeforeClass + public static void setGitDefaults() throws Exception { + CliGitCommand gitCmd = new CliGitCommand(null); + gitCmd.setDefaults(); + } + + @Test + public void roundtrip() throws Exception { + GitStep step = new GitStep("git@github.com:jenkinsci/workflow-plugin.git"); + Step roundtrip = new StepConfigTester(r).configRoundTrip(step); + r.assertEqualDataBoundBeans(step, roundtrip); + } + + @Test + public void roundtrip_withcredentials() throws Exception { + IdCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null, null, "user", "pass"); + CredentialsProvider.lookupStores(r.jenkins).iterator().next() + .addCredentials(Domain.global(), c); + GitStep step = new GitStep("git@github.com:jenkinsci/workflow-plugin.git"); + step.setCredentialsId(c.getId()); + Step roundtrip = new StepConfigTester(r).configRoundTrip(step); + r.assertEqualDataBoundBeans(step, roundtrip); + } + + @Test + public void basicCloneAndUpdate() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "demo"); + r.createOnlineSlave(Label.get("remote")); + p.setDefinition(new CpsFlowDefinition( + "node('remote') {\n" + + " ws {\n" + + " git(url: $/" + sampleRepo + "/$, poll: false, changelog: false)\n" + + " archive '**'\n" + + " }\n" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Cloning the remote Git repository", b); // GitSCM.retrieveChanges + assertTrue(b.getArtifactManager().root().child("file").isFile()); + sampleRepo.write("nextfile", ""); + sampleRepo.git("add", "nextfile"); + sampleRepo.git("commit", "--message=next"); + b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Fetching changes from the remote Git repository", b); // GitSCM.retrieveChanges + assertTrue(b.getArtifactManager().root().child("nextfile").isFile()); + } + + @Test + public void changelogAndPolling() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "demo"); + p.addTrigger(new SCMTrigger("")); // no schedule, use notifyCommit only + r.createOnlineSlave(Label.get("remote")); + p.setDefinition(new CpsFlowDefinition( + "node('remote') {\n" + + " ws {\n" + + " git($/" + sampleRepo + "/$)\n" + + " }\n" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("Cloning the remote Git repository", b); + sampleRepo.write("nextfile", ""); + sampleRepo.git("add", "nextfile"); + sampleRepo.git("commit", "--message=next"); + sampleRepo.notifyCommit(r); + b = p.getLastBuild(); + assertEquals(2, b.number); + r.assertLogContains("Fetching changes from the remote Git repository", b); + List> changeSets = b.getChangeSets(); + assertEquals(1, changeSets.size()); + ChangeLogSet changeSet = changeSets.get(0); + assertEquals(b, changeSet.getRun()); + assertEquals("git", changeSet.getKind()); + Iterator iterator = changeSet.iterator(); + assertTrue(iterator.hasNext()); + ChangeLogSet.Entry entry = iterator.next(); + assertEquals("[nextfile]", entry.getAffectedPaths().toString()); + assertFalse(iterator.hasNext()); + } + + @Test + public void multipleSCMs() throws Exception { + sampleRepo.init(); + otherRepo.init(); + otherRepo.write("otherfile", ""); + otherRepo.git("add", "otherfile"); + otherRepo.git("commit", "--message=init"); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "demo"); + p.addTrigger(new SCMTrigger("")); + p.setQuietPeriod(3); // so it only does one build + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " ws {\n" + + " dir('main') {\n" + + " git($/" + sampleRepo + "/$)\n" + + " }\n" + + " dir('other') {\n" + + " git($/" + otherRepo + "/$)\n" + + " }\n" + + " archive '**'\n" + + " }\n" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + VirtualFile artifacts = b.getArtifactManager().root(); + assertTrue(artifacts.child("main/file").isFile()); + assertTrue(artifacts.child("other/otherfile").isFile()); + sampleRepo.write("file2", ""); + sampleRepo.git("add", "file2"); + sampleRepo.git("commit", "--message=file2"); + otherRepo.write("otherfile2", ""); + otherRepo.git("add", "otherfile2"); + otherRepo.git("commit", "--message=otherfile2"); + sampleRepo.notifyCommit(r); + otherRepo.notifyCommit(r); + b = p.getLastBuild(); + assertEquals(2, b.number); + artifacts = b.getArtifactManager().root(); + assertTrue(artifacts.child("main/file2").isFile()); + assertTrue(artifacts.child("other/otherfile2").isFile()); + Iterator scms = p.getSCMs().iterator(); + assertTrue(scms.hasNext()); + assertEquals(sampleRepo.toString(), ((GitSCM) scms.next()).getRepositories().get(0).getURIs().get(0).toString()); + assertTrue(scms.hasNext()); + assertEquals(otherRepo.toString(), ((GitSCM) scms.next()).getRepositories().get(0).getURIs().get(0).toString()); + assertFalse(scms.hasNext()); + List> changeSets = b.getChangeSets(); + assertEquals(2, changeSets.size()); + ChangeLogSet changeSet = changeSets.get(0); + assertEquals(b, changeSet.getRun()); + assertEquals("git", changeSet.getKind()); + Iterator iterator = changeSet.iterator(); + assertTrue(iterator.hasNext()); + ChangeLogSet.Entry entry = iterator.next(); + assertEquals("[file2]", entry.getAffectedPaths().toString()); + assertFalse(iterator.hasNext()); + changeSet = changeSets.get(1); + iterator = changeSet.iterator(); + assertTrue(iterator.hasNext()); + entry = iterator.next(); + assertEquals("[otherfile2]", entry.getAffectedPaths().toString()); + assertFalse(iterator.hasNext()); + } + + @Issue("JENKINS-29326") + @Test + public void identicalGitSCMs() throws Exception { + sampleRepo.init(); + otherRepo.init(); + otherRepo.write("firstfile", ""); + otherRepo.git("add", "firstfile"); + otherRepo.git("commit", "--message=init"); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "demo"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " dir('main') {\n" + + " git($/" + otherRepo + "/$)\n" + + " }\n" + + " dir('other') {\n" + + " git($/" + otherRepo + "/$)\n" + + " }\n" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + assertEquals(1, b.getActions(BuildData.class).size()); + assertEquals(1, b.getActions(GitTagAction.class).size()); + assertEquals(0, b.getChangeSets().size()); + assertEquals(1, p.getSCMs().size()); + + otherRepo.write("secondfile", ""); + otherRepo.git("add", "secondfile"); + otherRepo.git("commit", "--message=second"); + WorkflowRun b2 = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + assertEquals(1, b2.getActions(BuildData.class).size()); + assertEquals(1, b2.getActions(GitTagAction.class).size()); + assertEquals(1, b2.getChangeSets().size()); + assertFalse(b2.getChangeSets().get(0).isEmptySet()); + assertEquals(1, p.getSCMs().size()); + } + + @Test + public void commitToWorkspace() throws Exception { + sampleRepo.init(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "def rungit(cmd) {def gitcmd = \"git ${cmd}\"; if (isUnix()) {sh gitcmd} else {bat gitcmd}}\n" + + "node {\n" + + " git url: $/" + sampleRepo + "/$\n" + + " writeFile file: 'file', text: 'edited by build'\n" + + " rungit 'commit --all --message=edits'\n" + + " rungit 'show master'\n" + + "}", true)); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); + r.assertLogContains("+edited by build", b); + } + +} diff --git a/src/test/java/jenkins/plugins/git/GitToolJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/GitToolJCasCCompatibilityTest.java new file mode 100644 index 0000000000..abe1deb6f7 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitToolJCasCCompatibilityTest.java @@ -0,0 +1,63 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.GitTool; +import hudson.tools.BatchCommandInstaller; +import hudson.tools.CommandInstaller; +import hudson.tools.InstallSourceProperty; +import hudson.tools.ToolDescriptor; +import hudson.tools.ToolInstallation; +import hudson.tools.ToolProperty; +import hudson.tools.ToolPropertyDescriptor; +import hudson.tools.ZipExtractionInstaller; +import hudson.util.DescribableList; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +public class GitToolJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + final ToolDescriptor descriptor = (ToolDescriptor) restartableJenkinsRule.j.jenkins.getDescriptor(GitTool.class); + final ToolInstallation[] installations = descriptor.getInstallations(); + assertThat(installations, arrayWithSize(1)); + assertEquals("Default", installations[0].getName()); + assertEquals("git", installations[0].getHome()); + final DescribableList, ToolPropertyDescriptor> properties = installations[0].getProperties(); + assertThat(properties, hasSize(1)); + final ToolProperty property = properties.get(0); + assertThat(((InstallSourceProperty)property).installers, + containsInAnyOrder( + allOf(instanceOf(CommandInstaller.class), + hasProperty("command", equalTo("install git")), + hasProperty("toolHome", equalTo("/my/path/1")), + hasProperty("label", equalTo("git command"))), + allOf(instanceOf(ZipExtractionInstaller.class), + hasProperty("url", equalTo("http://fake.com")), + hasProperty("subdir", equalTo("/my/path/2")), + hasProperty("label", equalTo("git zip"))), + allOf(instanceOf(BatchCommandInstaller.class), + hasProperty("command", equalTo("run batch command")), + hasProperty("toolHome", equalTo("/my/path/3")), + hasProperty("label", equalTo("git batch"))) + )); + } + + @Override + protected String stringInLogExpected() { + return ".installations = [GitTool[Default]]"; // Git client custom configurator supports JGit and JGit Apache + } + + @Override + protected String configResource() { + return "tool-casc.yaml"; + } +} diff --git a/src/test/java/jenkins/plugins/git/GlobalLibraryWithLegacyJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/GlobalLibraryWithLegacyJCasCCompatibilityTest.java new file mode 100644 index 0000000000..79868c3363 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GlobalLibraryWithLegacyJCasCCompatibilityTest.java @@ -0,0 +1,232 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.ChangelogToBranchOptions; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserMergeOptions; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.AssemblaWeb; +import hudson.plugins.git.extensions.impl.AuthorInChangelog; +import hudson.plugins.git.extensions.impl.ChangelogToBranch; +import hudson.plugins.git.extensions.impl.CheckoutOption; +import hudson.plugins.git.extensions.impl.CleanBeforeCheckout; +import hudson.plugins.git.extensions.impl.CleanCheckout; +import hudson.plugins.git.extensions.impl.CloneOption; +import hudson.plugins.git.extensions.impl.DisableRemotePoll; +import hudson.plugins.git.extensions.impl.GitLFSPull; +import hudson.plugins.git.extensions.impl.IgnoreNotifyCommit; +import hudson.plugins.git.extensions.impl.LocalBranch; +import hudson.plugins.git.extensions.impl.MessageExclusion; +import hudson.plugins.git.extensions.impl.PathRestriction; +import hudson.plugins.git.extensions.impl.PerBuildTag; +import hudson.plugins.git.extensions.impl.PreBuildMerge; +import hudson.plugins.git.extensions.impl.PruneStaleBranch; +import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; +import hudson.plugins.git.extensions.impl.ScmName; +import hudson.plugins.git.extensions.impl.SparseCheckoutPath; +import hudson.plugins.git.extensions.impl.SparseCheckoutPaths; +import hudson.plugins.git.extensions.impl.SubmoduleOption; +import hudson.plugins.git.extensions.impl.UserExclusion; +import hudson.plugins.git.extensions.impl.UserIdentity; +import hudson.plugins.git.extensions.impl.WipeWorkspace; +import hudson.scm.SCM; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; +import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; +import org.jenkinsci.plugins.workflow.libs.SCMRetriever; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class GlobalLibraryWithLegacyJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + final LibraryConfiguration library = GlobalLibraries.get().getLibraries().get(0); + assertEquals("My Git Lib", library.getName()); + assertEquals("1.2.3", library.getDefaultVersion()); + assertTrue(library.isImplicit()); + + final LibraryRetriever retriever = library.getRetriever(); + assertThat(retriever, instanceOf(SCMRetriever.class)); + final SCM scm = ((SCMRetriever) retriever).getScm(); + assertThat(scm, instanceOf(GitSCM.class)); + final GitSCM gitSCM = (GitSCM)scm; + + assertThat(gitSCM.getUserRemoteConfigs(), hasSize(1)); + final UserRemoteConfig userRemoteConfig = gitSCM.getUserRemoteConfigs().get(0); + assertEquals("acmeuser-cred-Id", userRemoteConfig.getCredentialsId()); + assertEquals("field_name", userRemoteConfig.getName()); + assertEquals("field_refspec", userRemoteConfig.getRefspec()); + assertEquals("https://git.acmecorp/myGitLib.git", userRemoteConfig.getUrl()); + + assertThat(gitSCM.getBranches(), hasSize(2)); + assertThat(gitSCM.getBranches(), containsInAnyOrder( + allOf(instanceOf(BranchSpec.class), + hasProperty("name", equalTo("master"))), + allOf(instanceOf(BranchSpec.class), + hasProperty("name", equalTo("myprodbranch"))) + )); + + assertThat(gitSCM.getBrowser(), instanceOf(AssemblaWeb.class)); + assertEquals("assemblaweb.url", gitSCM.getBrowser().getRepoUrl()); + + assertFalse(gitSCM.isDoGenerateSubmoduleConfigurations()); + + assertThat(gitSCM.getExtensions(), hasSize(22)); + assertThat(gitSCM.getExtensions(), containsInAnyOrder( + // Advanced checkout behaviours + allOf( + instanceOf(CheckoutOption.class), + hasProperty("timeout", equalTo(1)) + ), + // Advanced clone behaviours + allOf( + instanceOf(CloneOption.class), + hasProperty("shallow", equalTo(true)), + hasProperty("noTags", equalTo(false)), + hasProperty("reference", equalTo("/my/path/2")), + hasProperty("timeout", equalTo(2)), + hasProperty("depth", equalTo(2)), + hasProperty("honorRefspec", equalTo(true)) + ), + // Advanced sub-modules behaviours + allOf( + instanceOf(SubmoduleOption.class), + hasProperty("disableSubmodules", equalTo(true)), + hasProperty("parentCredentials", equalTo(true)), + hasProperty("recursiveSubmodules", equalTo(true)), + hasProperty("reference", equalTo("/my/path/3")), + hasProperty("timeout", equalTo(3)), + hasProperty("trackingSubmodules", equalTo(true)) + ), + // Calculate changelog against a specific branch + allOf( + instanceOf(ChangelogToBranch.class), + hasProperty("options", instanceOf(ChangelogToBranchOptions.class)), + hasProperty("options", hasProperty("compareRemote", equalTo("myrepo"))), + hasProperty("options", hasProperty("compareTarget", equalTo("mybranch"))) + ), + // Check out to a sub-directory + allOf( + instanceOf(RelativeTargetDirectory.class), + hasProperty("relativeTargetDir", equalTo("/my/path/5")) + ), + // Check out to specific local branch + allOf( + instanceOf(LocalBranch.class), + hasProperty("localBranch", equalTo("local_branch")) + ), + // Clean after checkout + allOf( + instanceOf(CleanCheckout.class) + ), + // Clean before checkout + allOf( + instanceOf(CleanBeforeCheckout.class) + ), + // Create a tag for every build + allOf( + instanceOf(PerBuildTag.class) + ), + // Don't trigger a build on commit notifications + allOf( + instanceOf(IgnoreNotifyCommit.class) + ), + // Force polling using workspace + allOf( + instanceOf(DisableRemotePoll.class) + ), + // Git LFS pull after checkout + allOf( + instanceOf(GitLFSPull.class) + ), + // Prune stale remote-tracking branches + allOf( + instanceOf(PruneStaleBranch.class) + ), + // Use commit author in changelog + allOf( + instanceOf(AuthorInChangelog.class) + ), + // Wipe out repository & force clone + allOf( + instanceOf(WipeWorkspace.class) + ), + // Custom SCM name + allOf( + instanceOf(ScmName.class), + hasProperty("name", equalTo("my_scm")) + ), + // Custom user name/e-mail address + allOf( + instanceOf(UserIdentity.class), + hasProperty("name", equalTo("custom_name")), + hasProperty("email", equalTo("custom@mail.com")) + ), + // Polling ignores commits from certain users + allOf( + instanceOf(UserExclusion.class), + hasProperty("excludedUsers", equalTo("me")) + ), + // Polling ignores commits in certain paths + allOf( + instanceOf(PathRestriction.class), + hasProperty("excludedRegions", equalTo("/path/excluded")), + hasProperty("includedRegions", equalTo("/path/included")) + ), + // Polling ignores commits with certain messages + allOf( + instanceOf(MessageExclusion.class), + hasProperty("excludedMessage", equalTo("message_excluded")) + ), + // Merge before build + allOf( + instanceOf(PreBuildMerge.class), + hasProperty("options", instanceOf(UserMergeOptions.class)), + hasProperty("options", hasProperty("fastForwardMode", equalTo(MergeCommand.GitPluginFastForwardMode.FF_ONLY))), + hasProperty("options", hasProperty("mergeRemote", equalTo("repo_merge"))), + hasProperty("options", hasProperty("mergeTarget", equalTo("branch_merge"))), + hasProperty("options", hasProperty("mergeStrategy", equalTo(MergeCommand.Strategy.OCTOPUS))) + ), + // Sparse Checkout paths + allOf( + instanceOf(SparseCheckoutPaths.class), + hasProperty("sparseCheckoutPaths", instanceOf(List.class)), + hasProperty("sparseCheckoutPaths", hasSize(2)), + hasProperty("sparseCheckoutPaths", containsInAnyOrder( + allOf( + instanceOf(SparseCheckoutPath.class), + hasProperty("path", equalTo("/first/last")) + ), + allOf( + instanceOf(SparseCheckoutPath.class), + hasProperty("path", equalTo("/other/path")) + ) + )) + ) + )); + } + + @Override + protected String stringInLogExpected() { + return "Setting class hudson.plugins.git.BranchSpec.name = myprodbranch"; + } + + @Override + protected String configResource() { + return "global-with-legacy-casc.yaml"; + } +} diff --git a/src/test/java/jenkins/plugins/git/GlobalLibraryWithModernJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/git/GlobalLibraryWithModernJCasCCompatibilityTest.java new file mode 100644 index 0000000000..db0ddbb38a --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GlobalLibraryWithModernJCasCCompatibilityTest.java @@ -0,0 +1,191 @@ +package jenkins.plugins.git; + +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.extensions.impl.CheckoutOption; +import hudson.plugins.git.extensions.impl.CloneOption; +import hudson.plugins.git.extensions.impl.SubmoduleOption; +import hudson.plugins.git.extensions.impl.UserIdentity; +import io.jenkins.plugins.casc.misc.RoundTripAbstractTest; +import jenkins.plugins.git.traits.AuthorInChangelogTrait; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.plugins.git.traits.CheckoutOptionTrait; +import jenkins.plugins.git.traits.CleanAfterCheckoutTrait; +import jenkins.plugins.git.traits.CleanBeforeCheckoutTrait; +import jenkins.plugins.git.traits.CloneOptionTrait; +import jenkins.plugins.git.traits.DiscoverOtherRefsTrait; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; +import jenkins.plugins.git.traits.GitLFSPullTrait; +import jenkins.plugins.git.traits.IgnoreOnPushNotificationTrait; +import jenkins.plugins.git.traits.LocalBranchTrait; +import jenkins.plugins.git.traits.PruneStaleBranchTrait; +import jenkins.plugins.git.traits.RefSpecsSCMSourceTrait; +import jenkins.plugins.git.traits.RemoteNameSCMSourceTrait; +import jenkins.plugins.git.traits.SubmoduleOptionTrait; +import jenkins.plugins.git.traits.TagDiscoveryTrait; +import jenkins.plugins.git.traits.UserIdentityTrait; +import jenkins.plugins.git.traits.WipeWorkspaceTrait; +import jenkins.scm.api.SCMSource; +import jenkins.scm.impl.trait.RegexSCMHeadFilterTrait; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; +import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; +import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class GlobalLibraryWithModernJCasCCompatibilityTest extends RoundTripAbstractTest { + @Override + protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { + final LibraryConfiguration library = GlobalLibraries.get().getLibraries().get(0); + assertEquals("My Git Lib", library.getName()); + assertEquals("1.2.3", library.getDefaultVersion()); + assertTrue(library.isImplicit()); + + final LibraryRetriever retriever = library.getRetriever(); + assertThat(retriever, instanceOf(SCMSourceRetriever.class)); + final SCMSource scm = ((SCMSourceRetriever) retriever).getScm(); + assertThat(scm, instanceOf(GitSCMSource.class)); + final GitSCMSource gitSCMSource = (GitSCMSource)scm; + + assertEquals("acmeuser-cred-Id", gitSCMSource.getCredentialsId()); + assertEquals("https://git.acmecorp/myGitLib.git", gitSCMSource.getRemote()); + + assertThat(gitSCMSource.getTraits(), hasSize(20)); + assertThat(gitSCMSource.getTraits(), containsInAnyOrder( + //Discover branches + allOf( + instanceOf(BranchDiscoveryTrait.class) + ), + // Discover tags + allOf( + instanceOf(TagDiscoveryTrait.class) + ), + // Check out to matching local branch + allOf( + instanceOf(LocalBranchTrait.class) + ), + // Clean after checkout + allOf( + instanceOf(CleanAfterCheckoutTrait.class) + ), + // Clean before checkout + allOf( + instanceOf(CleanBeforeCheckoutTrait.class) + ), + // Git LFS pull after checkout + allOf( + instanceOf(GitLFSPullTrait.class) + ), + // Ignore on push notifications + allOf( + instanceOf(IgnoreOnPushNotificationTrait.class) + ), + // Prune stale remote-tracking branches + allOf( + instanceOf(PruneStaleBranchTrait.class) + ), + // Use commit author in changelog + allOf( + instanceOf(AuthorInChangelogTrait.class) + ), + // Wipe out repository & force clone + allOf( + instanceOf(WipeWorkspaceTrait.class) + ), + // Discover other refs + allOf( + instanceOf(DiscoverOtherRefsTrait.class), + hasProperty("nameMapping", equalTo("mapping")), + hasProperty("ref", equalTo("other/refs")) + ), + // Filter by name (with regular expression) + allOf( + instanceOf(RegexSCMHeadFilterTrait.class), + hasProperty("regex", equalTo(".*acme*")) + ), + // Filter by name (with wildcards) + allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("excludes", equalTo("excluded")), + hasProperty("includes", equalTo("master")) + ), + // Configure remote name + allOf( + instanceOf(RemoteNameSCMSourceTrait.class), + hasProperty("remoteName", equalTo("other_remote")) + ), + // Advanced checkout behaviours + allOf( + instanceOf(CheckoutOptionTrait.class), + hasProperty("extension", instanceOf(CheckoutOption.class)), + hasProperty("extension", hasProperty("timeout", equalTo(1))) + ), + // Advanced clone behaviours + allOf( + instanceOf(CloneOptionTrait.class), + hasProperty("extension", instanceOf(CloneOption.class)), + hasProperty("extension", hasProperty("depth", equalTo(2))), + hasProperty("extension", hasProperty("honorRefspec", equalTo(true))), + hasProperty("extension", hasProperty("noTags", equalTo(false))), + hasProperty("extension", hasProperty("reference", equalTo("/my/path/2"))), + hasProperty("extension", hasProperty("shallow", equalTo(true))), + hasProperty("extension", hasProperty("timeout", equalTo(2))) + ), + // Advanced sub-modules behaviours + allOf( + instanceOf(SubmoduleOptionTrait.class), + hasProperty("extension", instanceOf(SubmoduleOption.class)), + hasProperty("extension", hasProperty("disableSubmodules", equalTo(true))), + hasProperty("extension", hasProperty("parentCredentials", equalTo(true))), + hasProperty("extension", hasProperty("recursiveSubmodules", equalTo(true))), + hasProperty("extension", hasProperty("reference", equalTo("/my/path/3"))), + hasProperty("extension", hasProperty("timeout", equalTo(3))), + hasProperty("extension", hasProperty("trackingSubmodules", equalTo(true))) + ), + // Configure Repository Browser + allOf( + instanceOf(GitBrowserSCMSourceTrait.class), + hasProperty("browser", instanceOf(BitbucketWeb.class)), + hasProperty("browser", hasProperty("repoUrl", equalTo("bitbucketweb.url"))) + ), + // Custom user name/e-mail address + allOf( + instanceOf(UserIdentityTrait.class), + hasProperty("extension", instanceOf(UserIdentity.class)), + hasProperty("extension", hasProperty("name", equalTo("my_user"))), + hasProperty("extension", hasProperty("email", equalTo("my@email.com"))) + ), + // Specify ref specs + allOf( + instanceOf(RefSpecsSCMSourceTrait.class), + hasProperty("templates", hasSize(1)), + hasProperty("templates", containsInAnyOrder( + allOf( + instanceOf(RefSpecsSCMSourceTrait.RefSpecTemplate.class), + hasProperty("value", equalTo("+refs/heads/*:refs/remotes/@{remote}/*")) + ) + )) + ) + )); + } + + @Override + protected String stringInLogExpected() { + return "Setting class jenkins.plugins.git.traits.UserIdentityTrait.extension = {}"; + } + + @Override + protected String configResource() { + return "global-with-modern-casc.yaml"; + } +} diff --git a/src/test/java/jenkins/plugins/git/ModernScmTest.java b/src/test/java/jenkins/plugins/git/ModernScmTest.java new file mode 100644 index 0000000000..d246b23e79 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/ModernScmTest.java @@ -0,0 +1,49 @@ +/* + * + * The MIT License + * + * Copyright (c) Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git; + +import hudson.ExtensionList; +import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + +public class ModernScmTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + @Issue("JENKINS-58964") + public void gitIsModernScm() { + SCMSourceRetriever.DescriptorImpl descriptor = ExtensionList.lookupSingleton(SCMSourceRetriever.DescriptorImpl.class); + assertThat(descriptor.getSCMDescriptors(), contains(instanceOf(GitSCMSource.DescriptorImpl.class))); + } +} diff --git a/src/test/java/jenkins/plugins/git/traits/DiscoverOtherRefsTraitTest.java b/src/test/java/jenkins/plugins/git/traits/DiscoverOtherRefsTraitTest.java new file mode 100644 index 0000000000..dd658114d5 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/traits/DiscoverOtherRefsTraitTest.java @@ -0,0 +1,50 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.plugins.git.traits; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class DiscoverOtherRefsTraitTest { + + @Test + public void getFullRefSpec() throws Exception { + DiscoverOtherRefsTrait t = new DiscoverOtherRefsTrait("refs/custom/*"); + assertEquals("+refs/custom/*:refs/remotes/@{remote}/custom/*", t.getFullRefSpec()); + } + + @Test + public void getNameMapping() throws Exception { + DiscoverOtherRefsTrait t = new DiscoverOtherRefsTrait("refs/bobby/*"); + assertEquals("bobby-@{1}", t.getNameMapping()); + t = new DiscoverOtherRefsTrait("refs/bobby/*/merge"); + assertEquals("bobby-@{1}", t.getNameMapping()); + t = new DiscoverOtherRefsTrait("refs/*"); + assertEquals("other-@{1}", t.getNameMapping()); + t = new DiscoverOtherRefsTrait("refs/bobby/all"); + assertEquals("other-ref", t.getNameMapping()); + } + +} \ No newline at end of file diff --git a/src/test/java/jenkins/plugins/git/traits/GitSCMExtensionTraitTest.java b/src/test/java/jenkins/plugins/git/traits/GitSCMExtensionTraitTest.java new file mode 100644 index 0000000000..c63e42f721 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/traits/GitSCMExtensionTraitTest.java @@ -0,0 +1,56 @@ +package jenkins.plugins.git.traits; + +import hudson.Util; +import hudson.model.Descriptor; +import hudson.plugins.git.extensions.GitSCMExtension; +import java.util.ArrayList; +import java.util.List; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class GitSCMExtensionTraitTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + public List descriptors() { + List list = new ArrayList<>(); + for (Descriptor d : SCMSourceTrait.all()) { + if (d instanceof GitSCMExtensionTraitDescriptor) { + list.add((GitSCMExtensionTraitDescriptor) d); + } + } + return list; + } + + @Test + public void extensionClassesOverrideEquals() { + for (GitSCMExtensionTraitDescriptor d : descriptors()) { + assertThat(d.getExtensionClass().getName() + " overrides equals(Object)", + Util.isOverridden(GitSCMExtension.class, d.getExtensionClass(), "equals", Object.class), + is(true)); + } + } + + @Test + public void extensionClassesOverrideHashCode() { + for (GitSCMExtensionTraitDescriptor d : descriptors()) { + assertThat(d.getExtensionClass().getName() + " overrides hashCode()", + Util.isOverridden(GitSCMExtension.class, d.getExtensionClass(), "hashCode"), + is(true)); + } + } + + @Test + public void extensionClassesOverrideToString() { + for (GitSCMExtensionTraitDescriptor d : descriptors()) { + assertThat(d.getExtensionClass().getName() + " overrides toString()", + Util.isOverridden(GitSCMExtension.class, d.getExtensionClass(), "toString"), + is(true)); + } + } +} diff --git a/src/test/java/jenkins/plugins/git/traits/PruneStaleBranchTraitTest.java b/src/test/java/jenkins/plugins/git/traits/PruneStaleBranchTraitTest.java new file mode 100644 index 0000000000..7b760d0556 --- /dev/null +++ b/src/test/java/jenkins/plugins/git/traits/PruneStaleBranchTraitTest.java @@ -0,0 +1,56 @@ +package jenkins.plugins.git.traits; + +import hudson.model.TaskListener; +import jenkins.plugins.git.GitSCMSourceContext; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; + +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; + +/** + * Test for JENKINS-57683 - Class cast exception when an SCMSource or + * SCMSourceContext was passed that was not a GitSCMSource. + * + * @author Mark Waite + */ +public class PruneStaleBranchTraitTest { + + public PruneStaleBranchTraitTest() { + } + + @Test + public void testDecorateContextWithGitSCMSourceContent() { + GitSCMSourceContext context = new GitSCMSourceContext(null, null); + assertThat(context.pruneRefs(), is(false)); + PruneStaleBranchTrait pruneStaleBranchTrait = new PruneStaleBranchTrait(); + pruneStaleBranchTrait.decorateContext(context); + assertThat(context.pruneRefs(), is(true)); + } + + @Test + @Issue("JENKINS-57683") + public void testDecorateContextWithNonGitSCMSourceContent() { + SCMSourceContext context = new FakeSCMSourceContext(null, null); + PruneStaleBranchTrait pruneStaleBranchTrait = new PruneStaleBranchTrait(); + pruneStaleBranchTrait.decorateContext(context); + /* JENKINS-57683 would cause this test to throw an exception */ + } + + private static class FakeSCMSourceContext extends SCMSourceContext { + + public FakeSCMSourceContext(SCMSourceCriteria scmsc, SCMHeadObserver scmho) { + super(scmsc, scmho); + } + + @Override + public SCMSourceRequest newRequest(SCMSource scms, TaskListener tl) { + throw new UnsupportedOperationException("Not supported yet."); + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/TestJGitAPIImpl.java b/src/test/java/org/jenkinsci/plugins/gitclient/TestJGitAPIImpl.java new file mode 100644 index 0000000000..3dddfcc599 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gitclient/TestJGitAPIImpl.java @@ -0,0 +1,21 @@ +package org.jenkinsci.plugins.gitclient; + +import hudson.model.TaskListener; +import jenkins.plugins.git.AbstractGitSCMSourceTest; +import org.jenkinsci.plugins.gitclient.jgit.PreemptiveAuthHttpClientConnectionFactory; + +import java.io.File; + +/** + * This is just here to make the constructors public + * @see AbstractGitSCMSourceTest#when_commits_added_during_discovery_we_do_not_crash() + */ +public class TestJGitAPIImpl extends JGitAPIImpl { + public TestJGitAPIImpl(File workspace, TaskListener listener) { + super(workspace, listener); + } + + public TestJGitAPIImpl(File workspace, TaskListener listener, PreemptiveAuthHttpClientConnectionFactory httpConnectionFactory) { + super(workspace, listener, httpConnectionFactory); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/gittagmessage/AbstractGitTagMessageExtensionTest.java b/src/test/java/org/jenkinsci/plugins/gittagmessage/AbstractGitTagMessageExtensionTest.java new file mode 100644 index 0000000000..12a599a388 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gittagmessage/AbstractGitTagMessageExtensionTest.java @@ -0,0 +1,168 @@ +package org.jenkinsci.plugins.gittagmessage; + +import hudson.model.Job; +import hudson.model.Queue; +import hudson.model.Run; +import hudson.plugins.git.util.BuildData; +import jenkins.model.ParameterizedJobMixIn; +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.IOException; + +import static org.junit.Assert.assertNotNull; + +public abstract class AbstractGitTagMessageExtensionTest & ParameterizedJobMixIn.ParameterizedJob, R extends Run & Queue.Executable> { + + @Rule public final JenkinsRule jenkins = new JenkinsRule(); + + @Rule public final TemporaryFolder repoDir = new TemporaryFolder(); + + private GitClient repo; + + /** + * @param refSpec The refspec to check out. + * @param branchSpec The branch spec to build. + * @param useMostRecentTag true to use the most recent tag rather than the exact one. + * @return A job configured with the test Git repo, given settings, and the Git Tag Message extension. + */ + protected abstract J configureGitTagMessageJob(String refSpec, String branchSpec, boolean useMostRecentTag) throws Exception; + + /** @return A job configured with the test Git repo, default settings, and the Git Tag Message extension. */ + private J configureGitTagMessageJob() throws Exception { + return configureGitTagMessageJob("", "**", false); + } + + /** Asserts that the given build exported tag information, or not, if {@code null}. */ + protected abstract void assertBuildEnvironment(R run, String expectedName, String expectedMessage) throws Exception; + + @Before + public void setUp() throws IOException, InterruptedException { + // Set up a temporary git repository for each test case + repo = Git.with(jenkins.createTaskListener(), null).in(repoDir.getRoot()).getClient(); + repo.init(); + } + + @Test + public void commitWithoutTagShouldNotExportMessage() throws Exception { + // Given a git repo without any tags + repo.commit("commit 1"); + + // When a build is executed + J job = configureGitTagMessageJob(); + R build = buildJobAndAssertSuccess(job); + + // Then no git tag information should have been exported + assertBuildEnvironment(build, null, null); + } + + @Test + public void commitWithEmptyTagMessageShouldNotExportMessage() throws Exception { + // Given a git repo which has been tagged, but without a message + repo.commit("commit 1"); + repo.tag("release-1.0", null); + + // When a build is executed + J job = configureGitTagMessageJob(); + R run = buildJobAndAssertSuccess(job); + + // Then the git tag name message, but no message should have been exported + assertBuildEnvironment(run, "release-1.0", null); + } + + @Test + public void commitWithTagShouldExportMessage() throws Exception { + // Given a git repo which has been tagged + repo.commit("commit 1"); + repo.tag("release-1.0", "This is the first release. "); + + // When a build is executed + J job = configureGitTagMessageJob(); + R run = buildJobAndAssertSuccess(job); + + // Then the (trimmed) git tag message should have been exported + assertBuildEnvironment(run, "release-1.0", "This is the first release."); + } + + @Test + public void commitWithMultipleTagsShouldExportMessage() throws Exception { + // Given a commit with multiple tags pointing to it + repo.commit("commit 1"); + repo.tag("release-candidate-1.0", "This is the first release candidate."); + repo.tag("release-1.0", "This is the first release."); + // TODO: JGit seems to list tags in alphabetical order rather than in reverse chronological order + + // When a build is executed + J job = configureGitTagMessageJob(); + R run = buildJobAndAssertSuccess(job); + + // Then the most recent tag info should have been exported + assertBuildEnvironment(run, "release-1.0", "This is the first release."); + } + + @Test + public void jobWithMatchingTagShouldExportThatTagMessage() throws Exception { + // Given a commit with multiple tags pointing to it + repo.commit("commit 1"); + repo.tag("alpha/1", "Alpha #1"); + repo.tag("beta/1", "Beta #1"); + repo.tag("gamma/1", "Gamma #1"); + + // When a build is executed which is configured to only build beta/* tags + J job = configureGitTagMessageJob("+refs/tags/beta/*:refs/remotes/origin/tags/beta/*", + "*/tags/beta/*", false); + R run = buildJobAndAssertSuccess(job); + + // Then the selected tag info should be exported, even although it's not the latest tag + assertBuildEnvironment(run, "beta/1", "Beta #1"); + } + + @Test + public void commitWithTagOnPreviousCommitWithConfigurationOptInShouldExportThatTagMessage() throws Exception { + // Given a git repo which has been tagged on a previous commit + repo.commit("commit 1"); + repo.tag("release-1.0", "This is the first release"); + repo.commit("commit 2"); + + // When a build is executed + J job = configureGitTagMessageJob("", "**", true); + R run = buildJobAndAssertSuccess(job); + + // Then the git tag name message should be exported, even it is not on the current commit + assertBuildEnvironment(run, "release-1.0", "This is the first release"); + } + + @Test + public void commitWithMultipleTagsOnPreviousCommitWithConfigurationOptInShouldExportThatTagMessage() throws Exception { + // Given a git repo which has been tagged on a previous commit with multiple tags + repo.commit("commit 1"); + repo.tag("release-candidate-1.0", "This is the first release candidate."); + repo.tag("release-1.0", "This is the first release."); + repo.commit("commit 2"); + + // When a build is executed + J job = configureGitTagMessageJob("", "**", true); + R run = buildJobAndAssertSuccess(job); + + // Then the most recent git tag name message should be exported, even it is not on the current commit + assertBuildEnvironment(run, "release-1.0", "This is the first release."); + } + + /** + * Builds the given job and asserts that it succeeded, and the Git SCM ran. + * + * @param job The job to build. + * @return The build that was executed. + */ + private R buildJobAndAssertSuccess(J job) throws Exception { + R build = jenkins.buildAndAssertSuccess(job); + assertNotNull(build.getAction(BuildData.class)); + return build; + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/gittagmessage/GitTagMessageExtensionTest.java b/src/test/java/org/jenkinsci/plugins/gittagmessage/GitTagMessageExtensionTest.java new file mode 100644 index 0000000000..dd0c9d4465 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gittagmessage/GitTagMessageExtensionTest.java @@ -0,0 +1,62 @@ +package org.jenkinsci.plugins.gittagmessage; + +import hudson.Functions; +import hudson.Util; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.SubmoduleConfig; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.tasks.BatchFile; +import hudson.tasks.Builder; +import hudson.tasks.Shell; + +import java.util.Collections; + +import static org.jenkinsci.plugins.gittagmessage.GitTagMessageAction.ENV_VAR_NAME_MESSAGE; +import static org.jenkinsci.plugins.gittagmessage.GitTagMessageAction.ENV_VAR_NAME_TAG; + +public class GitTagMessageExtensionTest extends AbstractGitTagMessageExtensionTest { + + /** + * @param refSpec The refspec to check out. + * @param branchSpec The branch spec to build. + * @param useMostRecentTag true to use the most recent tag rather than the exact one. + * @return A job configured with the test Git repo, given settings, and the Git Tag Message extension. + */ + protected FreeStyleProject configureGitTagMessageJob(String refSpec, String branchSpec, boolean useMostRecentTag) throws Exception { + GitTagMessageExtension extension = new GitTagMessageExtension(); + extension.setUseMostRecentTag(useMostRecentTag); + UserRemoteConfig remote = new UserRemoteConfig(repoDir.getRoot().getAbsolutePath(), "origin", refSpec, null); + GitSCM scm = new GitSCM( + Collections.singletonList(remote), + Collections.singletonList(new BranchSpec(branchSpec)), + false, Collections.emptyList(), + null, null, + Collections.singletonList(extension)); + + FreeStyleProject job = jenkins.createFreeStyleProject(); + job.getBuildersList().add(createEnvEchoBuilder("tag", ENV_VAR_NAME_TAG)); + job.getBuildersList().add(createEnvEchoBuilder("msg", ENV_VAR_NAME_MESSAGE)); + job.setScm(scm); + return job; + } + + /** Asserts that the given build exported tag information, or not, if {@code null}. */ + protected void assertBuildEnvironment(FreeStyleBuild build, String expectedName, String expectedMessage) + throws Exception { + // In the freestyle shell step, unknown environment variables are returned as empty strings + jenkins.assertLogContains(String.format("tag='%s'", Util.fixNull(expectedName)), build); + jenkins.assertLogContains(String.format("msg='%s'", Util.fixNull(expectedMessage)), build); + } + + private static Builder createEnvEchoBuilder(String key, String envVarName) { + if (Functions.isWindows()) { + return new BatchFile(String.format("echo %s='%%%s%%'", key, envVarName)); + } + return new Shell(String.format("echo \"%s='${%s}'\"", key, envVarName)); + } + +} diff --git a/src/test/resources/hudson/plugins/git/GitSCMTest/old1.xml b/src/test/resources/hudson/plugins/git/GitSCMTest/old1.xml index 05a4b1a525..9b5b0175be 100644 --- a/src/test/resources/hudson/plugins/git/GitSCMTest/old1.xml +++ b/src/test/resources/hudson/plugins/git/GitSCMTest/old1.xml @@ -25,7 +25,7 @@ Default - https://github.com/jenkinsci/jenkins/ + https://github.com/jenkinsci/model-ant-project.git/ @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/test/resources/hudson/plugins/git/browser/rawchangelog-with-escape b/src/test/resources/hudson/plugins/git/browser/rawchangelog-with-escape new file mode 100644 index 0000000000..7be290d120 --- /dev/null +++ b/src/test/resources/hudson/plugins/git/browser/rawchangelog-with-escape @@ -0,0 +1,15 @@ +commit 396fc230a3db05c427737aa5c2eb7856ba72b05d +tree 196333547f8b9a5fcc8b1fffe4accb01da42c5a6 +parent f28f125f4cc3e5f6a32daee6a26f36f7b788b8ff +author Mirko Friedenhagen 1277411790 +0200 +committer Mirko Friedenhagen 1277411790 +0200 + + Github seems to have no URL for deleted files, so just return a difflink instead. + +:100644 100644 3f28ad75f5ecd5e0ea9659362e2eef18951bd451 2e0756cd853dccac638486d6aab0e74bc2ef4041 M src/main/java/hudson/plugins/git/browser/GithubWeb.java +:100644 100644 019d377767702b6c572fa4ae97c982e02dcd76ff 7c89764ba7a51c23e809b24376d90d7d06337434 M src/test/java/hudson/plugins/git/browser/conf%.txt +:100644 100644 019d377767702b6c572fa4ae97c982e02dcd76fb 7c89764ba7a51c23e809b24376d90d7d06337436 M src/test/java/hudson/plugins/git/browser/conf%%.txt +:100644 100644 019d377767702b6c572fa4ae97c982e02dcd76fc 7c89764ba7a51c23e809b24376d90d7d06337437 M src/test/java/hudson/plugins/git/browser/conf%abc%.txt +:100644 100644 019d377767702b6c572fa4ae97c982e02dcd76fd 7c89764ba7a51c23e809b24376d90d7d06337438 M src/test/java/hudson/plugins/git/browser/conf^%.txt +:100644 100644 019d377767702b6c572fa4ae97c982e02dcd76fa 7c89764ba7a51c23e809b24376d90d7d06337435 M src/test/java/hudson/plugins/git/browser/config file.txt +:000000 100644 0000000000000000000000000000000000000000 885ce99421b3ae3a413a5c7fb0cdf9ec477d3f64 A src/test/resources/hudson/plugins/git/browser/rawchangelog-with-escape diff --git a/src/test/resources/hudson/plugins/git/browser/scm.json b/src/test/resources/hudson/plugins/git/browser/scm.json deleted file mode 100644 index 17846a9263..0000000000 --- a/src/test/resources/hudson/plugins/git/browser/scm.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "": [ - "hudson.plugins.git.util.DefaultBuildChooser", - "1" - ], - "authorOrCommitter": false, - "branch": {"branch": "master"}, - "browser": { - "stapler-class": "this.is.not.a.valid.BrowserClass", - "url": "https://github.com/hudson/Hudson-GIT-plugin" - }, - "buildChooser": {"stapler-class": "hudson.plugins.git.util.DefaultBuildChooser"}, - "clean": false, - "excludedRegions": "", - "excludedUsers": "", - "gitTool": "Default", - "localBranch": "", - "pruneBranches": false, - "recursiveSubmodules": false, - "relativeTargetDir": "", - "repo": { - "name": "origin", - "refspec": "+refs/heads/*:refs/remotes/origin/*", - "url": "git@github.com:hudson/Hudson-GIT-plugin.git" - }, - "value": "2", - "wipeOutWorkspace": false -} \ No newline at end of file diff --git a/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm.zip b/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm.zip new file mode 100644 index 0000000000..27b78c36d3 Binary files /dev/null and b/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm.zip differ diff --git a/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm_repositories.zip b/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm_repositories.zip new file mode 100644 index 0000000000..a7e9b0e860 Binary files /dev/null and b/src/test/resources/jenkins/plugins/git/GitBranchSCMHeadTest/testMigrationNoBuildStorm_repositories.zip differ diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_extension.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_extension.xml new file mode 100644 index 0000000000..f1216bfd29 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_extension.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_trait.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_trait.xml new file mode 100644 index 0000000000..ecca380321 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v1_trait.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_extension.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_extension.xml new file mode 100644 index 0000000000..e4697868fc --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_extension.xml @@ -0,0 +1,10 @@ + + + + true + + + true + + + diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_trait.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_trait.xml new file mode 100644 index 0000000000..15c7816c11 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/cleancheckout_v2_trait.xml @@ -0,0 +1,14 @@ + + + + + true + + + + + true + + + + diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/modern.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/modern.xml new file mode 100644 index 0000000000..350d290b81 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/modern.xml @@ -0,0 +1,5 @@ + + 5b061c87-da5c-4d69-b9d5-b041d065c945 + git://git.test/example.git + + diff --git a/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/pimpped_out.xml b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/pimpped_out.xml new file mode 100644 index 0000000000..f85714da43 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/GitSCMSourceTraitsTest/pimpped_out.xml @@ -0,0 +1,103 @@ + + fd2380f8-d34f-48d5-8006-c34542bc4a89 + git://git.test/example.git + e4d8c11a-0d24-472f-b86b-4b017c160e9a + origin + +refs/heads/*:refs/remotes/origin/* + foo/* + bar/* + true + + foo + + + + 5 + + + true + true + origin/foo + 3 + 3 + true + + + true + true + true + origin/bar + true + 4 + true + 3 + 4 + + + + silly + non-relevant-to-scm-source + + + + irrelevant + + + ** + + + true + + + true + + + + irrelevant + + + bob + bob@example.com + + + + + + + foo + bar + recursive + NO_FF + + + + does-not-work + + + irrelevant/ + does/not/work + + + does not work + + + + + + path1 + + + path2 + + + + + + 2 + 68f1518d49ffb39ae00e01326d6277fdf89c8029 + + + + + + diff --git a/src/test/resources/jenkins/plugins/git/browsers-casc.yaml b/src/test/resources/jenkins/plugins/git/browsers-casc.yaml new file mode 100644 index 0000000000..d1e99adf54 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/browsers-casc.yaml @@ -0,0 +1,273 @@ +unclassified: + globalLibraries: + libraries: + - name: "withAssembla" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + assemblaWeb: + repoUrl: "http://url.assembla" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withFisheye" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + fisheye: + repoUrl: "http://url.fishEye/browse/foobar" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withKiln" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + kilnGit: + repoUrl: "http://url.kiln" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withMic" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + tfs2013: + repoUrl: "http://url.mic/_git/foobar/" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withBitbucket" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + bitbucketWeb: + repoUrl: "http://url.bitbucket" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withCGit" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + cGit: + repoUrl: "http://url.cgit" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitlib" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitBlitRepositoryBrowser: + projectName: "my_project" + repoUrl: "http://url.gitlib" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGithub" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + githubWeb: + repoUrl: "http://github.com" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitiles" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitiles: + repoUrl: "http://url.gitiles" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitlab" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitLab: + repoUrl: "http://gitlab.com" + version: "1.0" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitlist" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitList: + repoUrl: "http://url.gitlist" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitorious" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitoriousWeb: + repoUrl: "http://url.gitorious" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGitweb" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gitWeb: + repoUrl: "http://url.gitweb" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withGogsgit" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + gogsGit: + repoUrl: "http://url.gogs" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withPhab" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + phabricator: + repo: "my_repository" + repoUrl: "http://url.phabricator" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withRedmine" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + redmineWeb: + repoUrl: "http://url.redmineweb" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withRhodecode" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + rhodeCode: + repoUrl: "http://url.rhodecode" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withStash" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + stash: + repoUrl: "http://url.stash" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" + - name: "withViewgit" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/master" + browser: + viewGitWeb: + projectName: "my_other_project" + repoUrl: "http://url.viewgit" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + userRemoteConfigs: + - url: "https://git.acmecorp/myGitLib.git" diff --git a/src/test/resources/jenkins/plugins/git/configuration-as-code.yaml b/src/test/resources/jenkins/plugins/git/configuration-as-code.yaml new file mode 100644 index 0000000000..4e99674ac5 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/configuration-as-code.yaml @@ -0,0 +1,38 @@ +unclassified: + globalLibraries: + libraries: + - defaultVersion: "1.2.3" + name: "My Git Lib" + retriever: + legacySCM: + scm: + git: + branches: + - name: "*/myprodbranch" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + extensions: + - "cleanCheckout" + - "gitLFSPull" + - checkoutOption: + timeout: 60 + - userIdentity: + email: "customuser@acmecorp.com" + name: "custom user" + - preBuildMerge: + options: + mergeRemote: "myrepo" + mergeStrategy: RECURSIVE + mergeTarget: "master" + submoduleCfg: + - submoduleName: "submodule-1" + branches: + - "mybranch-1" + - "mybranch-2" + - submoduleName: "submodule-2" + branches: + - "mybranch-3" + - "mybranch-4" + userRemoteConfigs: + - credentialsId: "acmeuser-cred-Id" + url: "https://git.acmecorp/myGitLib.git" diff --git a/src/test/resources/jenkins/plugins/git/gitscm-casc.yaml b/src/test/resources/jenkins/plugins/git/gitscm-casc.yaml new file mode 100644 index 0000000000..5617e27f58 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/gitscm-casc.yaml @@ -0,0 +1,5 @@ +unclassified: + gitSCM: + createAccountBasedOnEmail: true + globalConfigEmail: "me@mail.com" + globalConfigName: "user_name" diff --git a/src/test/resources/jenkins/plugins/git/global-with-legacy-casc.yaml b/src/test/resources/jenkins/plugins/git/global-with-legacy-casc.yaml new file mode 100644 index 0000000000..514f96a6ff --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/global-with-legacy-casc.yaml @@ -0,0 +1,79 @@ +unclassified: + globalLibraries: + libraries: + - defaultVersion: "1.2.3" + implicit: true + name: "My Git Lib" + retriever: + legacySCM: + scm: + git: + branches: + - name: "master" + - name: "myprodbranch" + browser: + assemblaWeb: + repoUrl: "assemblaweb.url" + buildChooser: "default" + doGenerateSubmoduleConfigurations: false + extensions: + - checkoutOption: + timeout: 1 + - cloneOption: + depth: 2 + honorRefspec: true + noTags: false + reference: "/my/path/2" + shallow: true + timeout: 2 + - submoduleOption: + disableSubmodules: true + parentCredentials: true + recursiveSubmodules: true + reference: "/my/path/3" + timeout: 3 + trackingSubmodules: true + - changelogToBranch: + options: + compareRemote: "myrepo" + compareTarget: "mybranch" + - relativeTargetDirectory: + relativeTargetDir: "/my/path/5" + - localBranch: + localBranch: "local_branch" + - "cleanCheckout" + - "cleanBeforeCheckout" + - "perBuildTag" + - scmName: + name: "my_scm" + - userIdentity: + email: "custom@mail.com" + name: "custom_name" + - "ignoreNotifyCommit" + - "disableRemotePoll" + - "gitLFSPull" + - preBuildMerge: + options: + fastForwardMode: FF_ONLY + mergeRemote: "repo_merge" + mergeStrategy: OCTOPUS + mergeTarget: "branch_merge" + - userExclusion: + excludedUsers: "me" + - pathRestriction: + excludedRegions: "/path/excluded" + includedRegions: "/path/included" + - messageExclusion: + excludedMessage: "message_excluded" + - "pruneStaleBranch" + - sparseCheckoutPaths: + sparseCheckoutPaths: + - path: "/first/last" + - path: "/other/path" + - "authorInChangelog" + - "wipeWorkspace" + userRemoteConfigs: + - credentialsId: "acmeuser-cred-Id" + name: "field_name" + refspec: "field_refspec" + url: "https://git.acmecorp/myGitLib.git" \ No newline at end of file diff --git a/src/test/resources/jenkins/plugins/git/global-with-modern-casc.yaml b/src/test/resources/jenkins/plugins/git/global-with-modern-casc.yaml new file mode 100644 index 0000000000..3adb3eebf8 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/global-with-modern-casc.yaml @@ -0,0 +1,64 @@ +unclassified: + globalLibraries: + libraries: + - defaultVersion: "1.2.3" + implicit: true + name: "My Git Lib" + retriever: + modernSCM: + scm: + git: + credentialsId: "acmeuser-cred-Id" + id: "ccdc6d86-b3cd-405f-8247-c196519234b7" + remote: "https://git.acmecorp/myGitLib.git" + traits: + - "branchDiscoveryTrait" + - discoverOtherRefsTrait: + nameMapping: "mapping" + ref: "other/refs" + - "tagDiscoveryTrait" + - headRegexFilter: + regex: ".*acme*" + - headWildcardFilter: + excludes: "excluded" + includes: "master" + - checkoutOptionTrait: + extension: + timeout: 1 + - cloneOptionTrait: + extension: + depth: 2 + honorRefspec: true + noTags: false + reference: "/my/path/2" + shallow: true + timeout: 2 + - submoduleOptionTrait: + extension: + disableSubmodules: true + parentCredentials: true + recursiveSubmodules: true + reference: "/my/path/3" + timeout: 3 + trackingSubmodules: true + - "localBranchTrait" + - "cleanAfterCheckoutTrait" + - "cleanBeforeCheckoutTrait" + - gitBrowser: + browser: + bitbucketWeb: + repoUrl: "bitbucketweb.url" + - remoteName: + remoteName: "other_remote" + - userIdentityTrait: + extension: + email: "my@email.com" + name: "my_user" + - "gitLFSPullTrait" + - "ignoreOnPushNotificationTrait" + - "pruneStaleBranchTrait" + - refSpecs: + templates: + - value: "+refs/heads/*:refs/remotes/@{remote}/*" + - "authorInChangelogTrait" + - "wipeWorkspaceTrait" \ No newline at end of file diff --git a/src/test/resources/jenkins/plugins/git/tool-casc.yaml b/src/test/resources/jenkins/plugins/git/tool-casc.yaml new file mode 100644 index 0000000000..ae3ddae712 --- /dev/null +++ b/src/test/resources/jenkins/plugins/git/tool-casc.yaml @@ -0,0 +1,20 @@ +tool: + git: + installations: + - home: "git" + name: "Default" + properties: + - installSource: + installers: + - command: + command: "install git" + label: "git command" + toolHome: "/my/path/1" + - zip: + label: "git zip" + subdir: "/my/path/2" + url: "http://fake.com" + - batchFile: + command: "run batch command" + label: "git batch" + toolHome: "/my/path/3" diff --git a/src/test/resources/namespaceBranchRepo.ls-remote b/src/test/resources/namespaceBranchRepo.ls-remote new file mode 100644 index 0000000000..b9dbd1c6fe --- /dev/null +++ b/src/test/resources/namespaceBranchRepo.ls-remote @@ -0,0 +1,13 @@ +86e6eeccc0b3a5e1c8034ff51718b8843a755789 HEAD +1aeaf8635d2e419a6e9587fd1ed1ddcd445845d9 refs/heads/a_tests/b_namespace1/master +3e73b26f220d8ad3e517858ffbe83b837d7f04c5 refs/heads/a_tests/b_namespace2/master +73d4779eae7b6aed8191635f5debc2b37ad083d0 refs/heads/a_tests/b_namespace3/feature3 +d940b841b7a5488ab42772f263b107a58eba2621 refs/heads/a_tests/b_namespace3/master +74ae8c24eb2783794d969e7f1df7260a8aa06d6b refs/heads/b_namespace3/feature4 +0b97e49ddbf699cf7b6deb31982d9d568d5e30f4 refs/heads/b_namespace3/master +b00d0c59cf79da494640788cb23453a0315b9c41 refs/heads/branchForTagA +dde47cb9b577cb6b50597931cdead4255669f322 refs/heads/branchForTagBAnnotated +86e6eeccc0b3a5e1c8034ff51718b8843a755789 refs/heads/master +b00d0c59cf79da494640788cb23453a0315b9c41 refs/tags/TagA +a388aa2e007106f77416e48f0aa3536d89c660ff refs/tags/TagBAnnotated +dde47cb9b577cb6b50597931cdead4255669f322 refs/tags/TagBAnnotated^{} diff --git a/src/test/resources/namespaceBranchRepo.zip b/src/test/resources/namespaceBranchRepo.zip new file mode 100644 index 0000000000..164d02923c Binary files /dev/null and b/src/test/resources/namespaceBranchRepo.zip differ diff --git a/src/test/resources/namespaceBranchRepoCreate.sh b/src/test/resources/namespaceBranchRepoCreate.sh new file mode 100644 index 0000000000..fa9c1c01f9 --- /dev/null +++ b/src/test/resources/namespaceBranchRepoCreate.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +rm -rf repo +mkdir repo +cd repo +cp ../*.sh . + +echo "This is a test repository containing namespace branches used to test the git client plugin.\n" > readme.txt +echo "Execute create.sh in an empty folder to recreate this repository.\n\n" >> readme.txt +echo "Copy the namespaceBranchRepo.zip and namespaceBranchRepo.ls-remote to src/test/resources/.\n\n" >> readme.txt +git init +git add . +git commit -m "Initial commit" +#gitk --all & + +# Create branches +git checkout master +git checkout -b a_tests/b_namespace1/master +touch a +git add . +git commit -m "Local branch 'a_tests/b_namespace1/master'" + +git checkout master +git checkout -b a_tests/b_namespace2/master +touch a +git add . +git commit -m "Local branch 'a_tests/b_namespace2/master'" + +git checkout master +git checkout -b a_tests/b_namespace3/master +touch a +git add . +git commit -m "Local branch 'a_tests/b_namespace3/master'" + +git checkout master +git checkout -b b_namespace3/master +touch a +git add . +git commit -m "Local branch 'b_namespace3/master'" + +git checkout master +git checkout -b branchForTagA +touch a +git add . +git commit -m "Local branch 'branchForTagA'" +git tag "TagA" + +git checkout master +git checkout -b branchForTagBAnnotated +touch a +git add . +git commit -m "Local branch 'branchForTagBAnnotated'" +git tag -a TagBAnnotated -m 'TagBAnnotated' + + +# End +git checkout master + +# Create files for src/test/resources/ and cleanup +jar cvf ../namespaceBranchRepo.zip ./ +git ls-remote . > ../namespaceBranchRepo.ls-remote +cd .. +rm -rf ./repo \ No newline at end of file diff --git a/src/test/resources/specialBranchRepo.ls-remote b/src/test/resources/specialBranchRepo.ls-remote new file mode 100644 index 0000000000..daf9689210 --- /dev/null +++ b/src/test/resources/specialBranchRepo.ls-remote @@ -0,0 +1,29 @@ +9add6d0cc7f1e6b6a56ea46e5465a49b7cb27267 HEAD +9add6d0cc7f1e6b6a56ea46e5465a49b7cb27267 refs/heads/master +767e4df205f6a3efcc9cedde0f7b4e9bd08b0f29 refs/heads/origin/master +c068dc5727410538ae766cd7da7b1ae1aecac210 refs/heads/origin/xxx +ed69129c6af4d1383e83f4104f7790e3ac427d7b refs/heads/refs/heads/master +9358a091b1e88d3667eeea67f6aeb9331454b6f8 refs/heads/refs/heads/refs/heads/master +95e5df4c99888d8f5abe2ce79987f3044462cdaa refs/heads/refs/heads/refs/heads/xxx +cc0487de42f51d5b4fcf93d844de754048d7b6f0 refs/heads/refs/heads/xxx +49b7076090c448eaff6f5fa5ddce82dcda7c3053 refs/heads/refs/remotes/origin/master +a82567343e6c4dde123b9c358c6888a3b31b2a8b refs/heads/refs/remotes/origin/xxx +53c876c0c5717a826825eefd872f35829f7c4ca3 refs/heads/refs/tags/master +9938cfc559a91ac3b0382edd7abefb51259487a2 refs/heads/refs/tags/xxx +d5838a353213f347774e742882b2ac29628748e5 refs/heads/remotes/origin/master +627d3931cc8f4b074b163401d97f4b6a8acc851e refs/heads/remotes/origin/xxx +a05391f3aea99f404f7b60a3328b8926cf0b163d refs/heads/tags +39ecd0a4933828ad9ff708cca14624077d5195d1 refs/tags/master +a05391f3aea99f404f7b60a3328b8926cf0b163d refs/tags/master^{} +d8056b36b4567f90e97aa212fdee546a0ea7a9c4 refs/tags/origin/master +04bc1991012ce2ecefc43cef51d22a3888abb0fa refs/tags/origin/master^{} +d611dd7fda98933802340642c6738bddf710f5a1 refs/tags/refs/heads/master +45ac31d52ddd4bb2bfdd57bfd01199882d53135e refs/tags/refs/heads/master^{} +d6241a544540f3959573a745c696020f209a8110 refs/tags/refs/heads/refs/heads/master +36935b63c6bd45fbc913c6d17704bc38f81cda5c refs/tags/refs/heads/refs/heads/master^{} +7592a73e4164e23b91e5617554878d7042384d6f refs/tags/refs/remotes/origin/master +2f79fa122716db04a64c24c30aeabe58cd31d1e7 refs/tags/refs/remotes/origin/master^{} +3c48e6d14e5ed6c0f5124a3adcc9762b7965a08e refs/tags/refs/tags/master +fe1e57f5851d81b8069b909c3db1a1d936c00047 refs/tags/refs/tags/master^{} +02786f3623098f55c9fbb79365e5e38a7650b4cc refs/tags/remotes/origin/master +ea9bb111ebec3dc02db3f4c2a5ef087862dd8df2 refs/tags/remotes/origin/master^{} diff --git a/src/test/resources/specialBranchRepo.zip b/src/test/resources/specialBranchRepo.zip new file mode 100644 index 0000000000..b5f21e4ab1 Binary files /dev/null and b/src/test/resources/specialBranchRepo.zip differ diff --git a/src/test/resources/specialBranchRepoCreate.sh b/src/test/resources/specialBranchRepoCreate.sh new file mode 100644 index 0000000000..2de36584bb --- /dev/null +++ b/src/test/resources/specialBranchRepoCreate.sh @@ -0,0 +1,136 @@ +#!/bin/sh + +rm -rf repo +mkdir repo +cd repo +cp ../*.sh . + +echo "This is a test repository containing all kinds of (weird) branches, tags, etc. used to test the git client plugin.\n" > readme.txt +echo "Execute create.sh in an empty folder to recreate this repository.\n\n" >> readme.txt +echo "Copy the specialBranchRepo.zip and specialBranchRepo.ls-remote to src/test/resources/.\n\n" >> readme.txt +git init +git add . +git commit -m "Initial commit" +#gitk --all & + +# Create branches + +git checkout master +git checkout -b origin/master +touch a +git add . +git commit -m "Local branch 'origin/master'" + +git checkout master +git checkout -b origin/xxx +touch a +git add . +git commit -m "Local branch 'origin/xxx'" + +git checkout master +git checkout -b remotes/origin/master +touch a +git add . +git commit -m "Local branch 'remotes/origin/master'" + +git checkout master +git checkout -b remotes/origin/xxx +touch a +git add . +git commit -m "Local branch 'remotes/origin/xxx'" + +git checkout master +git checkout -b refs/heads/master +touch a +git add . +git commit -m "Local branch 'refs/heads/master'" + +git checkout master +git checkout -b refs/heads/xxx +touch a +git add . +git commit -m "Local branch 'refs/heads/xxx'" + +git checkout master +git checkout -b refs/remotes/origin/master +touch a +git add . +git commit -m "Local branch 'refs/remotes/origin/master'" + +git checkout master +git checkout -b refs/remotes/origin/xxx +touch a +git add . +git commit -m "Local branch 'refs/remotes/origin/xxx'" + +git checkout master +git checkout -b refs/heads/refs/heads/master +touch a +git add . +git commit -m "Local branch 'refs/heads/refs/heads/master'" + +git checkout master +git checkout -b refs/heads/refs/heads/xxx +touch a +git add . +git commit -m "Local branch 'refs/heads/refs/heads/xxx'" + +git checkout master +git checkout -b refs/tags/master +touch a +git add . +git commit -m "Local branch 'refs/tags/master'" + +git checkout master +git checkout -b refs/tags/xxx +touch a +git add . +git commit -m "Local branch 'refs/tags/xxx'" + +# Create Tags +git checkout master +git checkout -b tags + +touch a +git add . +git commit -m "Tag test 'origin/master'" +git tag -a "origin/master" -m "Tag test 'origin/master'" + +touch b +git add . +git commit -m "Tag test 'remotes/origin/master'" +git tag -a "remotes/origin/master" -m "Tag test 'remotes/origin/master'" + +touch c +git add . +git commit -m "Tag test 'refs/heads/master'" +git tag -a "refs/heads/master" -m "Tag test 'refs/heads/master'" + +touch d +git add . +git commit -m "Tag test 'refs/remotes/origin/master'" +git tag -a "refs/remotes/origin/master" -m "Tag test 'refs/remotes/origin/master'" + +touch e +git add . +git commit -m "Tag test 'refs/heads/refs/heads/master'" +git tag -a "refs/heads/refs/heads/master" -m "Tag test 'refs/heads/refs/heads/master'" + +touch f +git add . +git commit -m "Tag test 'refs/tags/master'" +git tag -a "refs/tags/master" -m "Tag test 'refs/tags/master'" + +touch g +git add . +git commit -m "Tag test 'master'" +git tag -a "master" -m "Tag test 'master'" + +# End +git checkout master + +# Create files for src/test/resources/ and cleanup +jar cvf ../specialBranchRepo.zip ./ +git ls-remote . > ../specialBranchRepo.ls-remote +cd .. +rm -rf ./repo \ No newline at end of file