diff --git a/gradle.properties b/gradle.properties index c617bba..a21df3f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,3 +9,5 @@ mail.version = 1.6.2 sshd.version = 2.6.0.0 log4j.version = 2.14.0 junit4.version = 4.13 +jgit.version = 5.13.0.202109080827-r +spock.version = 1.2-groovy-2.5 diff --git a/groovy-git/LICENSE.txt b/groovy-git/LICENSE.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/groovy-git/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/groovy-git/NOTICE.txt b/groovy-git/NOTICE.txt new file mode 100644 index 0000000..1a4e1e1 --- /dev/null +++ b/groovy-git/NOTICE.txt @@ -0,0 +1,14 @@ +This work is a fork of Andrew J. Oberstar's grgit project as of 6-Oct-2018 + +https://github.com/ajoberstar/grgit + +Copyight (C) 2013 Andrew J. Oberstar + +License: Apache 2.0 + +This implementation is a fork of the Andrew J. Oberstar's grgit project with a reset of the +version numbering scheme. +It was ensured to build and run under Java 11 and Gradle 5. +Windows implementation and documentation has been removed. +The package structure and documentation have been adjusted; +all references to original authors and versions have been removed for clarity and consistency. \ No newline at end of file diff --git a/groovy-git/README.adoc b/groovy-git/README.adoc new file mode 100644 index 0000000..4e31ae7 --- /dev/null +++ b/groovy-git/README.adoc @@ -0,0 +1,20 @@ +# Groovy Git + +NOTE: This implementation is a fork of the Andrew J. Oberstar's https://github.com/ajoberstar/grgit[grgit] project with a reset of the +version numbering scheme. +It was ensured to build and run under Java 11 and Gradle 5. +Windows implementation details and documentation has been removed. +The package structure and documentation have been adjusted; +all references to original authors and versions have been removed for clarity and consistency. + +https://eclipse.org/jgit/[JGit] provides a powerful Java API for interacting with Git repositories. However, +in a Groovy context, it feels very cumbersome, making it harder to express the operations you want to perform +without being surrounded by a lot of cruft. + +Groovy Git is a wrapper over JGit that provides a fluent API for interacting with Git repositories in Groovy-based +tooling. Features that require more user interaction (such as resolving merge conflicts) are intentionally excluded. + +It also provides a Gradle plugin to easily get a Groovy Git instance. + +Groovy Git is available from Maven or the Gradle Plugin Portal. + diff --git a/groovy-git/build.gradle b/groovy-git/build.gradle new file mode 100644 index 0000000..4fe51e2 --- /dev/null +++ b/groovy-git/build.gradle @@ -0,0 +1,8 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api "org.eclipse.jgit:org.eclipse.jgit:${project.property('jgit.version')}" + testImplementation("org.spockframework:spock-core:${project.property('spock.version')}") { + exclude group: 'org.codehaus.groovy', module: 'groovy-all' + } +} diff --git a/groovy-git/src/docs/asciidoc/add.adoc b/groovy-git/src/docs/asciidoc/add.adoc new file mode 100644 index 0000000..659e673 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/add.adoc @@ -0,0 +1,60 @@ += add + +== Name + +add - Add file contents to the index + +== Synopsis + +[source, groovy] +---- +git.add(patterns: ['', ...], update: ) +---- + +[source, groovy] +---- +git.add { + patterns = ['', ...] + update = +} +---- + +== Description + +This command updates the index using the current content found in the working tree, to prepare the content staged for the next commit. It typically adds the current content of existing paths as a whole, but with some options it can also be used to remove paths that do not exist in the working tree anymore. + +The "index" holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit. Thus after making any changes to the working tree, and before running the commit command, you must use the add command to add any new or modified files to the index. + +This command can be performed multiple times before a commit. It only adds the content of the specified file(s) at the time the add command is run; if you want subsequent changes included in the next commit, then you must run `add` again to add the new content to the index. + +The status command can be used to obtain a summary of which files have changes that are staged for the next commit. + +Please see commit for alternative ways to add content to a commit. + +== Options + +patterns:: (`Set`, default `[]`) Files to add content from. A leading directory name (e.g. `dir` to add `dir/file1` and `dir/file2`) can be given to update the index to match the current state of the directory as a whole (e.g. specifying `dir` will record not just a file `dir/file1` modified in the working tree, a file `dir/file2` added to the working tree, but also a file `dir/file3` removed from the working tree. +update:: (`boolean`, default `false`) Update the index just where it already has an entry matching `patterns`. This removes as well as modifies index entries to match the working tree, but adds no new files. ++ +If no `pathspec` is given when `update` option is used, all tracked files in the entire working tree are updated. + +== Examples + +To add specific files or directories to the path. Wildcards are not supported. + +[source, groovy] +---- +git.add(patterns: ['1.txt', 'some/dir']) +---- + +To add changes to all currently tracked files. + +[source, groovy] +---- +git.add(update: true) +---- + +== See Also + +- link:https://git-scm.com/docs/git-add[git-add] + diff --git a/groovy-git/src/docs/asciidoc/apply.adoc b/groovy-git/src/docs/asciidoc/apply.adoc new file mode 100644 index 0000000..b7cd52f --- /dev/null +++ b/groovy-git/src/docs/asciidoc/apply.adoc @@ -0,0 +1,41 @@ += apply + +== Name + +apply - Apply a patch to files. + +== Synopsis + +[source, groovy] +---- +git.apply(patch: +} +---- + +== Description + +Reads the supplied diff output (i.e. "a patch") and applies it to files in the repository. + +This command applies the patch but does not create a commit. + +== Options + +patch:: (`Object`, default: `null`) The file to read the patch from. This can be a `java.io.File`, `java.nio.file.Path`, or a `String`. + +== Examples + +[source, groovy] +---- +git.apply(patch: '/some/file.patch') +---- + +== See Also + +- link:https://git-scm.com/docs/git-apply[git-apply] + diff --git a/groovy-git/src/docs/asciidoc/authentication.adoc b/groovy-git/src/docs/asciidoc/authentication.adoc new file mode 100644 index 0000000..77f0cc9 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/authentication.adoc @@ -0,0 +1,74 @@ += authentication + +== Description + +Groovy Git communicates with remote repositories in one of three ways: + +. HTTP(S) protocol with basic auth credentials (i.e. username/password). ++ +TIP: Where possible, use of HTTPS urls is preferred over the SSH options. It has far simpler behavior than the SSH options. +. SSH protocol using `ssh` directly. This approach should work as long as you can push/pull on your machine. ++ +TIP: You should be using an SSH agent to have your key loaded before Groovy Git runs. + +== HTTP BasicAuth Credentials + +These are presented in precedence order (direct parameter in code, system properties, environment variables). + +=== Parameter to Groovy Git operations + +Some Groovy Git operations, such as link:clone.html[clone] allow you to provide credentials programmatically. + +[source, groovy] +---- +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Credentials + +def git = Git.clone(dir: '...', url: '...', credentials: new Credentials(username, password)) +---- + +=== System Properties + +org.xbib.groovy.git.auth.username:: Username to provide when basic authentication credentials are needed. +Username can be specified without a password (e.g. using a GitHub auth token as the user and providing no password). +org.xbib.groovy.git.auth.password:: Password to provide when basic authentication credentials are needed. + +=== Environment Variables + +GIT_USER:: Username to provide when basic authentication credentials are needed. +Username can be specified without a password (e.g. using a GitHub auth token as the user and providing no password). +GIT_PASS:: Password to provide when basic authentication credentials are needed. + +== SSH + +If the `GIT_SSH` environment variable is set, that command will be used. +If not, Git will scan your `PATH` for an `ssh`command. + +Any keys your `ssh` or `plink` can natively pick up will be used. This should include keys in default locations +(e.g. `~/`) and any compatible SSH agent or OS keychain. + +[CAUTION] +==== +Groovy Git cannot provide credentials to the system `ssh` command. +If your key is password protected and not loaded in an agent, authentication _will_ fail **or** hang indefinitely. +==== + +== Examples + +This is a non-exhaustive list of examples of how to configure authentication in common scenarios. + +=== Using a GitHub auth token with HTTPS URLs + +Set the environment variable `GIT_USER` to your authentication token from GitHub. + +=== Using a Username and Password with HTTPS URLs + +Set the system properties: + +---- +groovy -Dorg.xbib.groovy.git.auth.username=someone -Dorg.xbib.groovy.git.auth.password=mysecretpassword myscript.groovy +---- + +=== Using ssh-agent + +Make sure your ssh-agent is started and your key is loaded. Then just run your application or script. diff --git a/groovy-git/src/docs/asciidoc/branch.adoc b/groovy-git/src/docs/asciidoc/branch.adoc new file mode 100644 index 0000000..658089b --- /dev/null +++ b/groovy-git/src/docs/asciidoc/branch.adoc @@ -0,0 +1,186 @@ += branch + +== Name + +branch - List, create, or delete branches + +== Synopsis + +[source, groovy] +---- +git.branch.current() +---- + +[source, groovy] +---- +git.branch.list() +git.branch.list(mode: , contains: ) +---- + +[source, groovy] +---- +git.branch.add(name: , startPoint: , mode: ) +---- + +[source, groovy] +---- +git.branch.change(name: , startPoint: , mode: ) +---- + +[source, groovy] +---- +git.branch.remove(names: [, ...], force: ) +---- + +== Description + + +`git.branch.current()`:: Returns the current branch. +`git.branch.list()`:: Returns a list of branches. The branches returned can be filtered using `mode` and `contains`. +`git.branch.add(name: , startPoint: , mode: )`:: Creates a new branch named `` pointing at ``, possibly tracking a remote depending on ``. Returns the created branch. +`git.branch.change(name: , startPoint: , mode: )`:: Modify an existing branch named `` pointing at ``, possibly tracking a remote depending on ``. Returns the modified branch. +`git.branch.remove(names: [, ...], force: )`:: Removes one or more branches. Returns a `List` of branch names removed. + +== Options + +=== list + +mode:: (`String`, default `local`) Must be one of `local`, `remote`, `all`. +`local`:::: Only list local branches (i.e. those under `refs/heads`) +`remote`:::: Only list remote branches (i.e. those under `refs/remotes`) +`all`:::: List all branches +contains:: (`Object`, default `null`) Only list branches which contain the specified commit. (`Object`, default `null`) Start the new branch at this commit. For a more complete list of acceptable inputs, see link:resolve.html[resolve] (specifically the `toRevisionString` method). + +=== add or change + +name:: (`String`, default `null`) Name of the branch +startPoint:: (`Object`, default `null`) Start the new branch at this commit. For a more complete list of acceptable inputs, see link:resolve.html[resolve] (specifically the `toRevisionString` method). +mode:: (`String`, default `null`) Must be one of `'track'` or `'no-track'` or `null` +`track`:::: When creating a new branch, set up `branch..remote` and `branch..merge` configuration entries to mark the start-point branch as "upstream" from the new branch. It directs link:pull.html[pull] without arguments to pull from the upstream when the new branch is checked out. ++ +This behavior is the default when the start point is a remote-tracking branch. Set the `branch.autoSetupMerge` configuration variable to `false` if you want link:checkout.html[checkout] and `branch` to always behave as if `no-track` were given. Set it to `always` if you want this behavior when the start-point is either a local or remote-tracking branch. +`no-track`:::: Do not set up "upstream" configuration, even if the branch.autoSetupMerge configuration variable is true. + +=== remove + +names:: (`List`, default `[]`) Names of the branches. For a more complete list of acceptable inputs, see link:resolve.html[resolve] (specifically the `toBranchName` method). +force:: (`boolean`, default `false`) Set to `true` to allow deleting branches that are not merged into another branch already. + +== Examples + +To add a branch starting at the current HEAD. + +[source, groovy] +---- +git.branch.add(name: 'new-branch') +---- + +To add a branch starting at, but not tracking, a local start point. + +[source, groovy] +---- +git.branch.add(name: 'new-branch', startPoint: 'local-branch') +git.branch.add(name: 'new-branch', startPoint: 'local-branch', mode: BranchAddOp.Mode.NO_TRACK) +---- + +To add a branch starting at and tracking a local start point. + +[source, groovy] +---- +git.branch.add(name: 'new-branch', startPoint: 'local-branch', mode: BranchAddOp.Mode.TRACK) +---- + +To add a branch starting from and tracking a remote branch. + +[source, groovy] +---- +git.branch.add(name: 'new-branch', startPoint: 'origin/remote-branch') +git.branch.add(name: 'new-branch', startPoint: 'origin/remote-branch', mode: BranchAddOp.Mode.TRACK) +---- + +To add a branch starting from, but not tracking, a remote branch. + +[source, groovy] +---- +git.branch.add(name: 'new-branch', startPoint: 'origin/remote-branch', mode: BranchAddOp.Mode.NO_TRACK) +---- + +To change the branch to start at, but not track, a local start point. + +[source, groovy] +---- +git.branch.change(name: 'existing-branch', startPoint: 'local-branch') +git.branch.change(name: 'existing-branch', startPoint: 'local-branch', mode: BranchChangeOp.Mode.NO_TRACK) +---- + +To change the branch to start at and track a local start point. + +[source, groovy] +---- +git.branch.change(name: 'existing-branch', startPoint: 'local-branch', mode: BranchChangeOp.Mode.TRACK) +---- + +To change the branch to start from and track a remote start point. + +[source, groovy] +---- +git.branch.change(name: 'existing-branch', startPoint: 'origin/remote-branch') +git.branch.change(name: 'existing-branch', startPoint: 'origin/remote-branch', mode: BranchChangeOp.Mode.TRACK) +---- + +To change the branch to start from, but not track, a remote start point. + +[source, groovy] +---- +git.branch.change(name: 'existing-branch', startPoint: 'origin/remote-branch', mode: BranchChangeOp.Mode.NO_TRACK) +---- + +Remove branches that have been merged. + +[source, groovy] +---- +def removedBranches = git.branch.remove(names: ['the-branch']) +def removedBranches = git.branch.remove(names: ['the-branch', 'other-branch'], force: false) +---- + +Remove branches, even if they haven't been merged. + +[source, groovy] +---- +def removedBranches = git.branch.remove(names: ['the-branch'], force: true) +---- + +To list local branches only. + +[source, groovy] +---- +def branches = git.branch.list() +def branches = git.branch.list(mode: BranchListOp.Mode.LOCAL) +---- + +To list remote branches only. + +[source, groovy] +---- +def branches = git.branch.list(mode: BranchListOp.Mode.REMOTE) +---- + +To list all branches. + +[source, groovy] +---- +def branches = git.branch.list(mode: BranchListOp.Mode.ALL) +---- + +To list all branches contains specified commit + +[source, groovy] +---- +def branches = git.branch.list(contains: %Commit hash or tag name%) +---- + +== See Also + +- link:https://git-scm.com/docs/git-branch[git-branch] +- link:push.html[push] +- link:pull.html[pull] diff --git a/groovy-git/src/docs/asciidoc/checkout.adoc b/groovy-git/src/docs/asciidoc/checkout.adoc new file mode 100644 index 0000000..68239a7 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/checkout.adoc @@ -0,0 +1,90 @@ += checkout + +== Name + +checkout - Switch branches + +== Synopsis + +[source, groovy] +---- +git.checkout(branch: ) +git.checkout(branch: , createBranch: true, orphan: , startPoint: ) +---- + +[source, groovy] +---- +git.checkout { + branch = +} + +git.checkout { + branch = + createBranch = true + orphan = + startPoint = +} +---- + +== Description + +Updates files in the working tree to match the version in the index or the specified tree. If no paths are given, git checkout will also update HEAD to set the specified branch as the current branch. + +`git.checkout(branch: )`:: To prepare for working on ``, switch to it by updating the index and the files in the working tree, and by pointing `HEAD` at the branch. Local modifications to the files in the working tree are kept, so that they can be committed to the ``. +`git.checkout(branch: , createBranch: true, startPoint: , orphan: )`:: Causes a new branch to be created as if link:branch.html[branch] were called and then checked out. + +== Options + +branch:: (`Object`, default `null`) Name of new branch to create or branch to checkout; if it refers to a branch (i.e., a name that, when prepended with "refs/heads/", is a valid ref), then that branch is checked out. Otherwise, if it refers to a valid commit, your HEAD becomes "detached" and you are no longer on any branch (see below for details). For a more complete list of acceptable inputs, see link:resolve.html[resolve] (specifically the `toBranchName` method). +startPoint:: (`Object`, default `null`) Start the new branch at this commit. For a more complete list of acceptable inputs, see link:resolve.html[git-resolve] (specifically the `toRevisionString` method). +createBranch:: (`boolean`, default `false`) Create a new branch named `` and start it at ``. +orphan:: (`boolean`, default `false`) Create a new orphan branch, named `branch`, started from `startPoint` and switch to it. The first commit made on this new branch will have no parents and it will be the root of a new history totally disconnected from all the other branches and commits. ++ +The index and the working tree are adjusted as if you had previously run `git.checkout(branch: )`. This allows you to start a new history that records a set of paths similar to `startPoint` by easily running `git.commit(message: , all: true)`. ++ +This can be useful when you want to publish the tree from a commit without exposing its full history. You might want to do this to publish an open source branch of a project whose current tree is "clean", but whose full history contains proprietary or otherwise encumbered bits of code. ++ +If you want to start a disconnected history that records a set of paths that is totally different from the one of `startPoint`, then you should clear the index and the working tree right after creating the orphan branch by running `git.remove(patterns: ['.'])` from the top level of the working tree. Afterwards you will be ready to prepare your new files, repopulating the working tree, by copying them from elsewhere, extracting a tarball, etc. + +== Examples + +To checkout an existing branch. + +[source, groovy] +---- +git.checkout(branch: 'existing-branch') +git.checkout(branch: 'existing-branch', createBranch: false) +---- + +To checkout a new branch starting at, but not tracking, the current HEAD. + +[source, groovy] +---- +git.checkout(branch: 'new-branch', createBranch: true) +---- + +To checkout a new branch starting at, but not tracking, a start point. + +[source, groovy] +---- +git.checkout(branch: 'new-branch', startPoint: 'any-branch', createBranch: true) +---- + +To checkout a new orphan branch starting at, but not tracking, the current HEAD. + +[source, groovy] +---- +git.checkout(branch: 'new-branch', orphan: true) +---- + +To checkout a new orphan branch starting at, but not tracking, a start point. + +[source, groovy] +---- +git.checkout(branch: 'new-branch', startPoint: 'any-branch', orphan: true) +---- + +== See Also + +- link:https://git-scm.com/docs/git-checkout[git-checkout] +- link:reset.html[reset] diff --git a/groovy-git/src/docs/asciidoc/clean.adoc b/groovy-git/src/docs/asciidoc/clean.adoc new file mode 100644 index 0000000..1f54e71 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/clean.adoc @@ -0,0 +1,85 @@ += clean + +== Name + +clean - Remove untracked files from the working tree + +== Synopsis + +[source, groovy] +---- +git.clean() +---- + +[source, groovy] +---- +git.clean(paths: ['', ...], directories: , dryRun: , ignore: ) +---- + +[source, groovy] +---- +git.clean { + paths = ['', ...] + directories = + dryRun = + ignore = +} +---- + +== Description + +Cleans the working tree by removing files that are not under version control. + +Normally, only files uknown to Git are removed, but if `ignore` is `false`, ignored files are also removed. This can, for example, be useful to remove all build products. + +If any optional `paths` are given, only those paths are affected. + +Returns a `Set` of the paths that were deleted. + +== Options + +directories:: (`boolean`, default `false`) Remove untracked directories in addition to untracked files. +dryRun:: (`boolean`, default `false`) Don’t actually remove anything, just show what would be done. +ignore:: (`boolean`, default `true`) Don’t use the standard ignore rules read from .gitignore (per directory) and $GIT_DIR/info/exclude. This allows removing all untracked files, including build products. This can be used (possibly in conjunction with git reset) to create a pristine working directory to test a clean build. +paths:: (`Set`, default `null`) Only remove files in the given paths. + +== Examples + +To clean all untracked files, but not ignored ones or untracked directories. + +[source, groovy] +---- +def cleanedPaths = git.clean() +---- + +To clean all untracked files and directories. + +[source, groovy] +---- +def cleanedPaths = git.clean(directories: true) +---- + +To clean all untracked files, including ignored ones. + +[source, groovy] +---- +def cleanedPaths = git.clean(ignore: false) +---- + +To only return files that would be cleaned. + +[source, groovy] +---- +def cleanedPaths = git.clean(dryRun: true) +---- + +To clean specific untracked files. + +[source, groovy] +---- +def cleanedPaths = git.clean(paths: ['specific/file.txt']) +---- + +== See Also + +- link:https://git-scm.com/docs/git-clean[git-clean] diff --git a/groovy-git/src/docs/asciidoc/clone.adoc b/groovy-git/src/docs/asciidoc/clone.adoc new file mode 100644 index 0000000..e2784a2 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/clone.adoc @@ -0,0 +1,52 @@ += clone + +== Name + +clone - Clone a repository into a new directory + +== Synopsis + +[source, groovy] +---- +git.clone(dir: , uri: , remote: , bare: , + checkout: , refToCheckout: , credentials: ) +---- + +[source, groovy] +---- +git.clone { + dir = + uri = + remote = + bare = + checkout = + refToCheckout = + credentials = +} +---- + +== Description + +Clones a repository into a newly created directory, creates remote-tracking branches for each branch in the cloned repository, and creates and checks out an initial branch that is forked from the cloned repository’s currently active branch. + +After the clone, a plain link:fetch.html[fetch] without arguments will update all the remote-tracking branches, and a link:pull.html[pull] without arguments will in addition merge the remote master branch into the current master branch. + +This default configuration is achieved by creating references to the remote branch heads under `refs/remotes/origin` and by initializing `remote.origin.url` and `remote.origin.fetch` configuration variables. + +Returns a Groovy Git instance. + +== Options + +dir:: (`Object`, default `null`) The directory the repository should be cloned into. Can be a `File`, `Path`, or `String`. +uri:: (`String`, default `null`) The URI to the repository to be cloned. +remote:: (`String`, default `origin`) Instead of using the remote name `origin` to keep track of the upstream repository, use ``. +bare:: (`boolean`, default `false`) Create a bare repository. +checkout:: (`boolean`, default `true`) Set to `false` to skip checking out a `HEAD`. +refToCheckout:: (`String`, default `null`) Instead of pointing the newly created `HEAD` to the branch pointed to by the cloned repository’s `HEAD`, point to `` branch instead. In a non-bare repository, this is the branch that will be checked out. This can also take tags and detaches the `HEAD` at that commit in the resulting repository. +credentials:: (`Credentials`, default `null`) An instance of Credentials containing username/password to be used in operations that require authentication. See link:authentication.html[authentication] for preferred ways to configure this. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-clone[git-clone] diff --git a/groovy-git/src/docs/asciidoc/commit.adoc b/groovy-git/src/docs/asciidoc/commit.adoc new file mode 100644 index 0000000..00be8d6 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/commit.adoc @@ -0,0 +1,85 @@ += commit + +== Name + +commit - Record changes to the repository + +== Synopsis + +[source, groovy] +---- +git.commit(message: , paths: [, ...], all: , amend: , + reflogComment: , committer: , author: ) +---- + +[source, groovy] +---- +git.commit { + message = + paths = [, ...] + all = + amend = + reflogComment = + committer = + author = +} +---- + +== Description + +Stores the current contents of the index in a new commit along with a log message from the user describing the changes. + +The content to be added can be specified in several ways: + +. by using link:add.html[add] to incrementally "add" changes to the index before using the commit command (Note: even modified files must be "added"); +. by using link:remove.html[remove] to remove files from the working tree and the index, again before using the commit command; +. by listing `paths` as arguments to the `commit` command, in which case the commit will ignore changes staged in the index, and instead record the current content of the listed files (which must already be known to Git); +. by using the `all` switch with the commit command to automatically "add" changes from all known files (i.e. all files that are already listed in the index) and to automatically "rm" files in the index that have been removed from the working tree, and then perform the actual commit; + +If you make a commit and then find a mistake immediately after that, you can recover from it with link:reset.html[reset]. + +Returns a Commit representing the new `HEAD`. + +== Options + +message:: (`String`, default `null`) Use the given as the commit message. +paths:: (`Set`, default `[]`) When files are given on the command line, the command commits the contents of the named files, without recording the changes already staged. The contents of these files are also staged for the next commit on top of what have been staged before. +all:: (`boolean`, default `false`) Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected. +amend:: (`boolean`, default `false`) Replace the tip of the current branch by creating a new commit. The recorded tree is prepared as usual, and the message from the original commit is used as the starting point, instead of an empty message, when no other message is specified from the command line. The new commit has the same parents and author as the current one. +reflogComment:: (`String`, default `null`) Use a different comment in the reflog for this commit. +committer:: (`Person`, default `null`) Override the committer recorded in the commit. This must be a Person. +author:: (`Person`, default `null`) Override the author recorded in the commit. This must be a Person. + +== Examples + +To commit all staged changes. + +[source, groovy] +---- +def commit = git.commit(message: 'something about the change') +---- + +To commit changes to all previously tracked files. + +[source, groovy] +---- +def commit = git.commit(message: 'something about the change', all: true) +---- + +To amend the previous commit. + +[source, groovy] +---- +def commit = git.commit(message: 'something about the change', amend: true) +---- + +To commit changes authored by another person. + +[source, groovy] +---- +def commit = git.commit(message: 'something about the change', author: new Person('Bruce Wayne', 'bruce.wayne@wayneindustries.com')) +---- + +== See Also + +- link:https://git-scm.com/docs/git-commit[git-commit] diff --git a/groovy-git/src/docs/asciidoc/describe.adoc b/groovy-git/src/docs/asciidoc/describe.adoc new file mode 100644 index 0000000..53fbefc --- /dev/null +++ b/groovy-git/src/docs/asciidoc/describe.adoc @@ -0,0 +1,67 @@ += describe + +== Name + +describe - Describe a commit using the most recent tag reachable from it + +== Synopsis + +[source, groovy] +---- +git.describe() +---- + +[source, groovy] +---- +git.describe(commit: , longDescr: , tags: , match: []) +---- + +[source, groovy] +---- +git.describe { + commit = + longDescr = + tags = + match = [] +} +---- + +== Description + +The command finds the most recent tag that is reachable from a commit. If the tag points to the commit, then only the tag is shown. Otherwise, it suffixes the tag name with the number of additional commits on top of the tagged object and the abbreviated object name of the most recent commit. + +Describe only shows annotated tags. For more information about creating annotated tags see the `annotate` option to link:tag.html[tag]. + +== Options + +commit:: (`Object`, default `null`) Commit-ish object names to describe. Defaults to HEAD if omitted. For a more complete list of ways to spell commit names, see link:resolve.html[resolve] (specifically the `toCommit` method). +longDescr:: (`boolean`, default `false`) Always output the long format (the tag, the number of commits and the abbreviated commit name) even when it matches a tag. This is useful when you want to see parts of the commit object name in "describe" output, even when the commit in question happens to be a tagged version. Instead of just emitting the tag name, it will describe such a commit as v1.2-0-gdeadbee (0th commit since tag v1.2 that points at object deadbee…​.). +tags:: (`boolean`, default `false`) Instead of using only the annotated tags, use any tag found in `refs/tags` namespace. This option enables matching a lightweight (non-annotated) tag. +match:: (`List`, default `[]`) Only consider tags matching the given glob(7) pattern, excluding the "refs/tags/" prefix. This can be used to avoid leaking private tags from the repository. If multiple patterns are given they will be accumulated, and tags matching any of the patterns will be considered. + +== Examples + +Describe the current `HEAD`. + +[source, groovy] +---- +git.describe() == '1.0.0' +---- + +Find the most recent tag that is reachable from a different commit. + +[source, groovy] +---- +git.describe(commit: 'other-branch') == '2.0.0-rc.1-7-g91fda36' +---- + +Always output the long format (the tag, the number of commits and the abbreviated commit name) even when it matches a tag. + +[source, groovy] +---- +git.describe(longDescr: true) == '2.0.0-rc.1-7-g91fda36' +---- + +== See Also + +- link:https://git-scm.com/docs/git-describe[git-describe] diff --git a/groovy-git/src/docs/asciidoc/fetch.adoc b/groovy-git/src/docs/asciidoc/fetch.adoc new file mode 100644 index 0000000..ad752a7 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/fetch.adoc @@ -0,0 +1,58 @@ += fetch + +== Name + +fetch - Download objects and refs from another repository + +== Synopsis + +[source, groovy] +---- +git.fetch() +---- + +[source, groovy] +---- +git.fetch(remote: '', refSpecs: [, ...], prune: , tagMode: ) +---- + +[source, groovy] +---- +git.fetch { + remote = '' + refspecs = [, ...] + prune = + tagMode = +} +---- + +== Description + +Fetch branches and/or tags (collectively, "refs") from one or more other repositories, along with the objects necessary to complete their histories. Remote-tracking branches are updated (see the description of below for ways to control this behavior). + +By default, any tag that points into the histories being fetched is also fetched; the effect is to fetch tags that point at branches that you are interested in. This default behavior can be changed by using a `tagMode` of `'all'` or `'none'' or by configuring `remote..tagOpt`. By using a refspec that fetches tags explicitly, you can fetch tags that do not point into branches you are interested in as well. + +When no remote is specified, by default the origin remote will be used, unless there’s an upstream branch configured for the current branch. + +== Options + +remote:: (`String`, default `null`) The "remote" repository that is source of a fetch operation. This parameter can be either a URL or the name of a remote. +refspecs:: (`List`, default `[]`) Specifies which refs to fetch and which local refs to update. When no s are provided, the refs to fetch are read from `remote..fetch` variables instead. ++ +The format of a parameter is an optional plus +, followed by the source ref , followed by a colon :, followed by the destination ref . The colon can be omitted when is empty. ++ +The remote ref that matches is fetched, and if is not empty string, the local ref that matches it is fast-forwarded using . If the optional plus + is used, the local ref is updated even if it does not result in a fast-forward update. +prune:: (`boolean`, default `false`) Before fetching, remove any remote-tracking references that no longer exist on the remote. Tags are not subject to pruning if they are fetched only because of the default tag auto-following or due to a `tagMode` option. However, if tags are fetched due to an explicit refspec, then they are also subject to pruning. +tagMode:: (`String`, default `auto`) Must be one of `'auto'`, `'all'`, `'none'`. ++ +`'auto'` - tags that point at objects that are downloaded from the remote repository are fetched and stored locally. ++ +`'none'` - disables automatic tag following. ++ +`'all'` - Fetch all tags from the remote (i.e., fetch remote tags `refs/tags/*` into local tags with the same name), in addition to whatever else would otherwise be fetched. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-fetch[git-fetch] diff --git a/groovy-git/src/docs/asciidoc/gradle.adoc b/groovy-git/src/docs/asciidoc/gradle.adoc new file mode 100644 index 0000000..f55b0f1 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/gradle.adoc @@ -0,0 +1,69 @@ += gradle + +## Applying the Plugin + +Generally, you should only apply the plugin to the root project of your build. + +```groovy +plugins { + id 'org.xbib.gradle.plugin.git' version '' +} +``` + +## What does the plugin do? + +The `org.xbib.gradle.plugin.git` plugin adds a `git` property to your build, +which is an instance of Groovy Git opened to the repository visible from your project's root dir. +This will check the project directory and its parents for a `.git` directory. +If no repository is found, the `git` property is `null`. + +```groovy +version = "1.0.0-${git.head().abbreviatedId}" + +task tagRelease { + description = 'Tags the current head with the project\'s version.' + doLast { + git.tag.add { + name = version + message = "Release of ${version}" + } + } +} + +task pushToOrigin { + description = 'Pushes current branch\'s committed changes to origin repo.' + doLast { + git.push() + } +} +``` + +For details on the available operations, see the link:reference.html[reference]. Examples are provided there. + +## Just getting the library + +If you don't want to interact with the project's repository, but still want to + +```groovy +plugins { + id 'org.xbib.gradle.plugin.git' version '' apply false +} +``` + +Then you can import Groovy Git and continue from there: + +```groovy +import org.xbib.gradle.plugin.git.Git + +task cloneSomeRepo { + doLast { + def git = Git.clone(dir: "$buildDir/my-repo", uri: "https://github.com/jprante/groovy-git.git") + println git.describe() + } +} +``` + +## Authentication + +If you will be doing a clone, fetch, push, or pull, review the link:authentication.html[authentication] page for details +on how to configure credentials for these commands. diff --git a/groovy-git/src/docs/asciidoc/head.adoc b/groovy-git/src/docs/asciidoc/head.adoc new file mode 100644 index 0000000..0524058 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/head.adoc @@ -0,0 +1,22 @@ += head + +== Name + +head - Gets the current `HEAD` commit. + +== Synopsis + +[source, groovy] +---- +git.head() +---- + +== Description + +Returns the commit that the current `HEAD` points to. + +== Options + +== Examples + +== See Also diff --git a/groovy-git/src/docs/asciidoc/index.adoc b/groovy-git/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..db70568 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/index.adoc @@ -0,0 +1,159 @@ += groovy-git + +== What is Groovy Git? + +A library providing a wrapper around link:https://eclipse.org/jgit/[Eclipse's JGit] for more fluent usage from Groovy. +This allows you to interact easily with link:https://git-scm.com[Git] repositories from general applications +using Groovy or from Gradle builds. + +== Where do I get Groovy Git? + +To use Groovy Git as a library, add a dependency on it. + +[source, groovy] +.build.gradle +---- + +dependencies { + compile 'org.xbib:groovy-git:' +} +---- + +[source, xml] +.pom.xml +---- + + + + org.xbib + groovy-git + ... + + +---- + +To use Groovy Git in a Gradle build, see the documentation for link:gradle.html[org.xbib.gradle.plugin.git]. + +== How do I use Groovy Git? + +First you need to get an instance of Groovy Git, by either link:init.html[initializing] a new repo or +link:open.html[opening] or link:clone.html[cloning] an existing repo. + +Once you have a Groovy Git instance, you can use the available operations to inspect or modify the repo. +Each operation has 3 variants, depending on your preference: + +[source, groovy] +---- +// no arg variant, only applies to operations that don't require input +git.log() + +// map argument variant +git.log(includes: ['master', 'other-branch'], excludes: ['old-stuff'], skipCommits: 5) + +// closure argument variant +git.log { + range('1.0.0', '1.5.1') + maxCommits = 2 +} +---- + +Look in the link:reference.html[reference] documentation for information on each available operation, +it's options, and example usage. Additionally, see the link:authentication.html[authentication] documentation +if you plan to interact with a remote repository. + +== Example Usage + +In an example like this, using the HTTP protocol, you'll likely need to specify basic auth credentials. +This can be done via system properties or environment variables. SSH access is also supported. +See the link:-authentication.html[authentication] documentation. + +[source] +.Environment Variables +---- +GIT_USER=somebody +GIT_PASS=myauthtoken +---- + +[source, groovy] +.gitSample.groovy +---- +// get an instance +def git = Git.clone(dir: 'test-repo', uri: 'https://github.com/jprante/groovy-git.git') + +// make some changes +new File('test-repo/file.txt') << 'making some changes' +git.add(patterns: ['test-repo/file.txt']) + +// make a commit +git.commit(message: 'Adding a new file') + +// view the commits +git.log { + range('origin/master', 'master') +}.each { commit -> + println "${commit.id} ${commit.shortMessage}" +} + +// push to the remote +git.push() + +// cleanup after yourself +git.close() +---- + +include::add.adoc[] + +include::apply.adoc[] + +include::authentication.adoc[] + +include::branch.adoc[] + +include::checkout.adoc[] + +include::clean.adoc[] + +include::clone.adoc[] + +include::commit.adoc[] + +include::describe.adoc[] + +include::fetch.adoc[] + +include::gradle.adoc[] + +include::head.adoc[] + +include::init.adoc[] + +include::isAncestorOf.adoc[] + +include::log.adoc[] + +include::lsremote.adoc[] + +include::merge.adoc[] + +include::open.adoc[] + +include::pull.adoc[] + +include::push.adoc[] + +include::remote.adoc[] + +include::remove.adoc[] + +include::reset.adoc[] + +include::resolve.adoc[] + +include::revert.adoc[] + +include::show.adoc[] + +include::status.adoc[] + +include::tag.adoc[] + diff --git a/groovy-git/src/docs/asciidoc/init.adoc b/groovy-git/src/docs/asciidoc/init.adoc new file mode 100644 index 0000000..7cc4f6b --- /dev/null +++ b/groovy-git/src/docs/asciidoc/init.adoc @@ -0,0 +1,37 @@ += init + +== Name + +init - Create an empty Git repository + +== Synopsis + +[source, groovy] +---- +git.init(dir: , bare: ) +---- + +[source, groovy] +---- +git.init { + dir = + bare = +} +---- + +== Description + +This command creates an empty Git repository - basically a `.git` directory with subdirectories for `objects`, `refs/heads`, `refs/tags`, and template files. An initial `HEAD` file that references the HEAD of the master branch is also created. + +Returns a Groovy Git instance. + +== Options + +dir:: (`Object`, default `null`) The directory the repository should be initialized in. Can be a `File`, `Path`, or `String`. +bare:: (`boolean`, default `false`) Create a bare repository. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-init[git-init] diff --git a/groovy-git/src/docs/asciidoc/isAncestorOf.adoc b/groovy-git/src/docs/asciidoc/isAncestorOf.adoc new file mode 100644 index 0000000..f788b4f --- /dev/null +++ b/groovy-git/src/docs/asciidoc/isAncestorOf.adoc @@ -0,0 +1,30 @@ += isAncestorOf + +== Name + +isAncestorOf - Tell if a commit is an ancestor of another. + +== Synopsis + +[source, groovy] +---- +git.isAncestorOf(, ) +---- + +== Description + +Given a base commit and a tip commit, return `true` if the base commit can be reached by walking back from the tip. + +== Options + +1. (`Object`) base commit. For a more complete list of objects you can pass in, see link:resolve.html[resolve] (specifically the `toCommit` method). +1. (`Object`) tip commit. For a more complete list of objects you can pass in, see link:resolve.html[resolve] (specifically the `toCommit` method). + +== Examples + +[source, groovy] +---- +git.isAncestorOf('v1.2.3', 'master') +---- + +== See Also diff --git a/groovy-git/src/docs/asciidoc/log.adoc b/groovy-git/src/docs/asciidoc/log.adoc new file mode 100644 index 0000000..dce2c80 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/log.adoc @@ -0,0 +1,71 @@ += log + +== Name + +log - Show commit logs + +== Synopsis + +[source, groovy] +---- +git.log() +---- + +[source, groovy] +---- +git.log(includes: [, ...], excludes: [, ...], paths: [, ...], + skipCommits: , maxCommits: ) +---- + +[source, groovy] +---- +git.log { + includes = [, ...] + excludes = [, ...] + paths = [, ...] + skipCommits = + maxCommits = + range(, ) +} +---- + +== Description + +Shows the commit logs. + +List commits that are reachable by following the parent links from the given `includes` commit(s), but exclude commits that are reachable from the one(s) given with the `excludes` option. The output is given in reverse chronological order. + +You can think of this as a set operation. Commits given in `includes` form a set of commits that are reachable from any of them, and then commits reachable from any of the ones given with `excludes` are subtracted from that set. The remaining commits are what comes out in the result. Various other options and paths parameters can be used to further limit the result. + +Thus, the following command means "list all the commits which are reachable from foo or bar, but not from baz". + +[source, groovy] +---- +git.log(includes: ['foo', 'bar'], excludes: ['baz']) +---- + +A special notation (in the Closure syntax only) `range(, )` can be used as a short-hand for `excludes: [], includes: []`. For example, either of the following may be used interchangeably: + +[source, groovy] +---- +git.log(includes: ['HEAD'], excludes: ['origin']) +git.log { + range('origin', 'HEAD') +} +---- + +Returns a `List` with the commits matching the criteria given. + +== Options + +includes:: (`List`, default `[]`) Commit-ish object names to include the history of in the output. For a more complete list of ways to spell commit names, see link:resolve.html[resolve] (specifically the `toCommit` method). +excludes:: (`List`, default `[]`) Commit-ish object names to exclude the history of in the output. For a more complete list of ways to spell commit names, see link:resolve.html[resolve] (specifically the `toCommit` method). +skipCommits:: (`int`, default `-1`) Skip `skipCommits` commits before starting to show the commit output. A negative value ignores this option. +maxCommits:: (`int`, default `-1`) Limit the number of commits to output. A negative value ignores this option. +paths:: (`List`, defaul: `[]`) Commits modifying the given paths are selected. Omitting this will include all reachable commits. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-log[git-log] diff --git a/groovy-git/src/docs/asciidoc/lsremote.adoc b/groovy-git/src/docs/asciidoc/lsremote.adoc new file mode 100644 index 0000000..422fa10 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/lsremote.adoc @@ -0,0 +1,58 @@ += lsremote + +== Name + +lsremote - List references in a remote repository + +== Synopsis + +[source, groovy] +---- +git.lsremote() +---- + +[source, groovy] +---- +git.lsremote(heads: , tags: , remote: '') +---- + +[source, groovy] +---- +git.lsremote { + heads = + tags = + remote = '' +} +---- + +== Description + +Returns a `Map` containing references available in the remote, and the object ID they currently point to. + +== Options + +heads:: (`boolean`, default `false`) Limit to `refs/heads`. This is not mutually exclusive with `tags`; when given both, references stored in both places are returned. +tags:: (`boolean`, default `false`) Limit to `refs/tags`. This is not mutually exclusive with `heads`; when given both, references stored in both places are returned. +remote:: (`String`, default `'origin'`) The name of the remote or the URI of the repository to list. + +== Examples + +[source, groovy] +.Code +---- +git.lsremote(tags: true).each { ref, id -> + println "${id} ${ref.fullName}" +} +---- + +.Output +---- +d6602ec5194c87b0fc87103ca4d67251c76f233a refs/tags/v0.99 +f25a265a342aed6041ab0cc484224d9ca54b6f41 refs/tags/v0.99.1 +7ceca275d047c90c0c7d5afb13ab97efdf51bd6e refs/tags/v0.99.3 +c5db5456ae3b0873fc659c19fafdde22313cc441 refs/tags/v0.99.2 +---- + +== See Also + +- link:https://git-scm.com/docs/git-ls-remote[git-ls-remote] diff --git a/groovy-git/src/docs/asciidoc/merge.adoc b/groovy-git/src/docs/asciidoc/merge.adoc new file mode 100644 index 0000000..c620981 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/merge.adoc @@ -0,0 +1,60 @@ += merge + +== Name + +merge - Join two or more development histories together + +== Synopsis + +[source, groovy] +---- +git.merge(head: , mode: , message: ) +---- + +[source, groovy] +---- +git.merge { + head = + mode = + message = +} +---- + +== Description + +Incorporates changes from the named commits (since the time their histories diverged from the current branch) into the current branch. This command is used by link:pull.html[pull] to incorporate changes from another repository and can be used by hand to merge changes from one branch into another. + +Assume the following history exists and the current branch is "master": + +---- + A---B---C topic + / + D---E---F---G master +---- + +Then `git.merge(head: 'topic')` will replay the changes made on the topic branch since it diverged from master (i.e., `E`) until its current commit `C` on top of master, and record the result in a new commit along with the names of the two parent commits and a log message from the user describing the changes. + +---- + A---B---C topic + / \ + D---E---F---G---H master +---- + +This is a simplified version of merge. If any conflict occurs the merge will throw an exception. The conflicting files can be identified with link:status.html[status]. + +== Options + +head:: (`Object`, default `null`) Commit, usually another branch head, to merge into our branch. For a more complete list of acceptable inputs, see link:resolve.html[resolve] (specifically the `toRevisionString` method). +mode:: (`String`, default `default`) Must be one of `default`, `only-ff`, `create-commit`, `squash`, `no-commit`. +`default`:::: When the merge resolves as a fast-forward, only update the branch pointer, without creating a merge commit. +`only-ff`:::: Refuse to merge and fail with an exception unless the current `HEAD` is already up-to-date or the merge can be resolved as a fast-forward. +`create-commit`:::: Create a merge commit even when the merge resolves as a fast-forward. +`squash`:::: Produce the working tree and index state as if a real merge happened (except for the merge information), but do not actually make a commit, move the `HEAD`, or record `$GIT_DIR/MERGE_HEAD` (to cause the next git commit command to create a merge commit). This allows you to create a single commit on top of the current branch whose effect is the same as merging another branch (or more in case of an octopus). +`no-commit`:::: Perform the merge but pretend the merge failed and do not autocommit, to give the user a chance to inspect and further tweak the merge result before committing. +message:: (`String`, default `null`) Use the given as the merge commit message. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-merge[git-merge] diff --git a/groovy-git/src/docs/asciidoc/open.adoc b/groovy-git/src/docs/asciidoc/open.adoc new file mode 100644 index 0000000..86d838e --- /dev/null +++ b/groovy-git/src/docs/asciidoc/open.adoc @@ -0,0 +1,42 @@ += open + +== Name + +open - Open an existing Git repository + +== Synopsis + +[source, groovy] +---- +git.open() +---- + +[source, groovy] +---- +git.open(dir: , currentDir: , credentials: ) +---- + +[source, groovy] +---- +git.open { + dir = + currentDir = + credentials = +} +---- + +== Description + +This command opens an existing Git repository. If both `dir` and `currentDir` are `null`, acts as if the `currentDir` is the JVM's working directory. + +Returns a Groovy Git instance. + +== Options + +dir:: (`Object`, default `null`) The directory the repository is in. Can be a `File`, `Path`, or `String`. +currentDir:: (`Object`, default `null`) The directory to start searching for the repository from. Can be a `File`, `Path`, or `String`. +credentials:: (`Credentials`, default `null`) An instance of Credentials containing username/password to be used in operations that require authentication. +See link:authentication.html[authentication] for preferred ways to configure this. + +== Examples + diff --git a/groovy-git/src/docs/asciidoc/pull.adoc b/groovy-git/src/docs/asciidoc/pull.adoc new file mode 100644 index 0000000..ab712ff --- /dev/null +++ b/groovy-git/src/docs/asciidoc/pull.adoc @@ -0,0 +1,64 @@ += pull + +== Name + +pull - Fetch from and integrate with another repository or a local branch + +== Synopsis + +[source, groovy] +---- +git.pull() +---- + +[source, groovy] +---- +git.pull(remote: '', branch: '', rebase: ) +---- + +[source, groovy] +---- +git.pull { + remote = '' + branch = '' + rebase = +} +---- + +== Description + +Incorporates changes from a remote repository into the current branch. In its default mode, git pull is shorthand for fetch followed by merge. + +More precisely, pull runs fetch with the given parameters and calls merge to merge the retrieved branch heads into the current branch. With `rebase`, it runs rebase instead of merge. + +Default values for `remote` and `branch` are read from the "remote" and "merge" configuration for the current branch. + +Assume the following history exists and the current branch is "master": + +---- + A---B---C master on origin + / + D---E---F---G master + ^ + origin/master in your repository +---- + +Then "git pull" will fetch and replay the changes from the remote master branch since it diverged from the local master (i.e., E) until its current commit `C` on top of master and record the result in a new commit along with the names of the two parent commits and a log message from the user describing the changes. + +---- + A---B---C origin/master + / \ + D---E---F---G---H master +---- + +== Options + +remote:: (`String`, default `null`) The "remote" repository that is source of a pull operation. This parameter can be either a URL or the name of a remote. +branch:: (`String`, default `null`) The remote branch to pull. +rebase:: (`boolean`, default `false`) When true, rebase the current branch on top of the upstream branch after fetching. If there is a remote-tracking branch corresponding to the upstream branch and the upstream branch was rebased since last fetched, the rebase uses that information to avoid rebasing non-local changes. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-pull[git-pull] diff --git a/groovy-git/src/docs/asciidoc/push.adoc b/groovy-git/src/docs/asciidoc/push.adoc new file mode 100644 index 0000000..196a850 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/push.adoc @@ -0,0 +1,63 @@ += push + +== Name + +git-push - Update remote refs along with associated objects + +== Synopsis + +[source, groovy] +---- +git.push() +---- + +[source, groovy] +---- +git.push(remote: '', refsOrSpecs: [, ...], + all: , tags: , force: , dryRun: ) +---- + +[source, groovy] +---- +git.push { + remote = '' + refsOrSpecs = [, ...] + all = + tags = + force = + dryRun = +} +---- + +== Description + +Updates remote refs using local refs, while sending objects necessary to complete the given refs. + +When the `remote` is not specified, `branch.*.remote` configuration for the current branch is consulted to determine where to push. If the configuration is missing, it defaults to `origin`. + +When the `refsOrSpecs` or `all` or `tags` options are not specified, the command finds the default `` by consulting `remote.*.push` configuration, and if it is not found, honors `push.default` configuration to decide what to push (See link:https://git-scm.com/docs/git-config[git-config] for the meaning of `push.default`). + +== Options + +remote:: (`String`, default `null`) The "remote" repository that is destination of a push operation. This parameter can be either a URL or the name of a remote. +refsOrSpecs:: (`List`, default `[]`) Specify what destination ref to update with what source object. The format of a parameter is an optional plus +, followed by the source object , followed by a colon :, followed by the destination ref . ++ +The is often the name of the branch you would want to push, but it can be any arbitrary "SHA-1 expression", such as `master~4` or `HEAD` (see link:https://git-scm.com/docs/gitrevisions[gitrevisions]). ++ +The tells which ref on the remote side is updated with this push. Arbitrary expressions cannot be used here, an actual ref must be named. If `git.push(remote: '')` without any `refsOrSpecs` argument is set to update some ref at the destination with with `remote..push` configuration variable, : part can be omitted—​such a push will update a ref that normally updates without any on the command line. Otherwise, missing : means to update the same ref as the . ++ +The object referenced by is used to update the reference on the remote side. By default this is only allowed if is not a tag (annotated or lightweight), and then only if it can fast-forward . By having the optional leading +, you can tell Git to update the ref even if it is not allowed by default (e.g., it is not a fast-forward.) This does not attempt to merge into . ++ +Pushing an empty allows you to delete the ref from the remote repository. ++ +The special refspec : (or +: to allow non-fast-forward updates) directs Git to push "matching" branches: for every branch that exists on the local side, the remote side is updated if a branch of the same name already exists on the remote side. +all:: (`boolean`, default `false`) Push all branches (i.e. refs under `refs/heads/`); cannot be used with other `refsOrSpecs`. +tags:: (`boolean`, default `false`) All refs under refs/tags are pushed, in addition to refspecs explicitly listed in `refsOrSpecs`. +force:: (`boolean`, default `false`) Usually, the command refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it. Note that `force` applies to all the refs that are pushed, hence using it with `push.default` set to `matching` or with multiple push destinations configured with `remote.*.push` may overwrite refs other than the current branch (including local refs that are strictly behind their remote counterpart). To force a push to only one branch, use a + in front of the refspec to push (e.g git push origin +master to force a push to the master branch). See the ... section above for details. +dryRun:: (`boolean`, default `false`) Do everything except actually send the updates. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-push[git-push] diff --git a/groovy-git/src/docs/asciidoc/remote.adoc b/groovy-git/src/docs/asciidoc/remote.adoc new file mode 100644 index 0000000..bdf4a2a --- /dev/null +++ b/groovy-git/src/docs/asciidoc/remote.adoc @@ -0,0 +1,55 @@ += remote + +== Name + +remote - Manage set of tracked repositories + +== Synopsis + +[source, groovy] +---- +git.remote.list() +---- + +[source, groovy] +---- +git.remote.add(name: , url: , pushUrl: , + fetchRefSpecs: [, ...], pushRefSpecs: [, ...], + mirror: ) +---- + +[source, groovy] +---- +git.remote.add { + name = + url = + pushUrl = + fetchRefSpecs = [, ...] + pushRefSpecs = [, ...] + mirror = +} +---- + +== Description + +Manage the set of repositories ("remotes") whose branches you track. + +`list()` returns a `List` (Remote) of all remotes configured for the repo. +`add()` returns the newly configured Remote + +== Options + +=== add + +name:: (`String`, default `null`) Name of the new remote +url:: (`String`, default `null`) URL to fetch the remote repository from. +pushUrl:: (`String`, default `null`) URL to push to for this remote. If omitted, `url` is used for push. +fetchRefSpecs:: (`List`, default `[+refs/heads/*:refs/remotes//*]`) default refspecs to use when fetching from this remote. +pushRefSpecs:: (`List`, default `[]`) default refspecs to use when pushing to this remote. +mirror:: (`boolean`, default `false`) If `true` makes push always behave as if `mirror` is specified. + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-remote[git-remote] diff --git a/groovy-git/src/docs/asciidoc/remove.adoc b/groovy-git/src/docs/asciidoc/remove.adoc new file mode 100644 index 0000000..386295a --- /dev/null +++ b/groovy-git/src/docs/asciidoc/remove.adoc @@ -0,0 +1,49 @@ += remove + +== Name + +remove - Remove files from the working tree and from the index + +== Synopsis + +[source, groovy] +---- +git.remove(patterns: ['', ...], cached: ) +---- + +[source, groovy] +---- +git.remove { + patterns = ['', ...] + cached = +} +---- + +== Description + +Remove files from the index, or from the working tree and the index. `remove` will not remove a file from just your working directory. The files being removed have to be identical to the tip of the branch, and no updates to their contents can be staged in the index. When `cached` is given, the staged content has to match either the tip of the branch or the file on disk, allowing the file to be removed from just the index. + +== Options + +patterns:: (`Set`, default `[]`) Files to remove. A leading directory name (e.g. `dir` to remove `dir/file1` and `dir/file2`) can be given to remove all files in the directory, and recursively all sub-directories. +cached:: (`boolean`, default `false`) Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone. + +== Examples + +Remove specific file or directory from both the index and working tree. + +[source, groovy] +---- +git.remove(patterns: ['1.txt', 'some/dir']) +---- + +Remove specific file or directory from the index, but leave the in the working tree. + +[source, groovy] +---- +git.remove(patterns: ['1.txt', 'some/dir'], cached: true) +---- + +== See Also + +- link:https://git-scm.com/docs/git-rm[git-rm] diff --git a/groovy-git/src/docs/asciidoc/reset.adoc b/groovy-git/src/docs/asciidoc/reset.adoc new file mode 100644 index 0000000..129b1a4 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/reset.adoc @@ -0,0 +1,88 @@ += reset + +== Name + +reset - Reset current HEAD to the specified state + +== Synopsis + +[source, groovy] +---- +git.reset(commit: , paths: [, ...]) +git.reset(mode: , commit: ) +---- + +[source, groovy] +---- +git.reset { + commit = + paths = [, ...] +} + +git.reset { + mode = + commit = +} +---- + +== Description + +In the first form, copy entries from `commit` to the index. In the second form, set the current branch head (HEAD) to `commit`, optionally modifying index and working tree to match. The `commit` defaults to `HEAD` in all forms. + +`git.reset(commit: , paths: [, ...])`:: ++ +This form resets the index entries for all `` to their state at ``. (It does not affect the working tree or the current branch.) ++ +This means that `git.reset(paths: [])` is the opposite of `git.add(patterns: [])`. ++ +After running `reset` to update the index entry, you can use checkout to check the contents out of the index to the working tree. Alternatively, using checkout and specifying a commit, you can copy the contents of a path out of a commit to the index and to the working tree in one go. +`git.reset(mode: , commit: )`:: ++ +This form resets the current branch head to `` and possibly updates the index (resetting it to the tree of ``) and the working tree depending on . If `mode` is omitted, defaults to `mixed`. The must be one of the following: ++ +`soft`:::: Does not touch the index file or the working tree at all (but resets the head to ``, just like all modes do). This leaves all your changed files `staged`, as `git.status()` would put it. +`mixed`:::: Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) and reports what has not been updated. This is the default action. +`hard`:::: Resets the index and working tree. Any changes to tracked files in the working tree since `` are discarded. + +If you want to undo a commit other than the latest on a branch, revert is your friend. + +== Options + +commit:: (`Object`, default `null`) Commit to reset to. For a more complete list of ways to spell commit names, see resolve (specifically the `toCommit` method). +paths:: (`Set`, default `[]`) Paths to reset. +mode:: (`String`, default `mixed`) Must be one of `hard`, `mixed`, `soft`. + +== Examples + +Reset the HEAD to a different commit. + +[source, groovy] +---- +git.reset(commit: 'HEAD~1', mode: 'soft') +---- + +Reset the HEAD, index, and working tree to a different commit. + +[source, groovy] +---- +git.reset(commit: 'other-branch', mode: 'hard') +---- + +Reset the HEAD and index to a different commit. + +[source, groovy] +---- +git.reset(commit: 'HEAD~2') +git.reset(commit: 'HEAD~2', mode: 'mixed') +---- + +Reset the index for specific paths back to the HEAD + +[source, groovy] +---- +git.reset(paths: ['some/file.txt']) +---- + +== See Also + +- link:https://git-scm.com/docs/git-reset[git-reset] diff --git a/groovy-git/src/docs/asciidoc/resolve.adoc b/groovy-git/src/docs/asciidoc/resolve.adoc new file mode 100644 index 0000000..b4572ed --- /dev/null +++ b/groovy-git/src/docs/asciidoc/resolve.adoc @@ -0,0 +1,66 @@ += resolve + +== Name + +git-resolve - Resolves objects to various types + +== Synopsis + +[source, groovy] +---- +git.resolve.toObjectId() +---- + +[source, groovy] +---- +git.resolve.toCommit() +---- + +[source, groovy] +---- +git.resolve.toBranch() +---- + +[source, groovy] +---- +git.resolve.toBranchName() +---- + +[source, groovy] +---- +git.resolve.toTag() +---- + +[source, groovy] +---- +git.resolve.toTagName() +---- + +[source, groovy] +---- +git.resolve.toRevisionString() +---- + +== Description + +Various methods to resolve objects to types needed by Groovy Git operations. These are used to normalize the input, allowing the caller more flexibility in providing the data they have rather than having to convert it ahead of time. + +== Options + +toObjectId:: Accepts Commit, Tag, Branch, Ref + +toCommit:: Accepts Commit, Tag, Branch, String, GString + +toBranch:: Accepts Branch, String, GString + +toBranchName:: Accepts Branch, String, GString + +toTag:: Accepts Tag, String, GString + +toTagName:: Accepts Tag, String, GString + +toRevisionString:: Accepts Commit, Tag, Branch, String, GString + +== Examples + +== See Also diff --git a/groovy-git/src/docs/asciidoc/revert.adoc b/groovy-git/src/docs/asciidoc/revert.adoc new file mode 100644 index 0000000..27b29ee --- /dev/null +++ b/groovy-git/src/docs/asciidoc/revert.adoc @@ -0,0 +1,43 @@ += revert + +== Name + +revert - Revert some existing commits + +== Synopsis + +[source, groovy] +---- +git.revert(commits: [, ...]) +---- + +[source, groovy] +---- +git.revert { + commits = ['', ...] +} +---- + +== Description + + +Given one or more existing commits, revert the changes that the related patches introduce, and record some new commits that record them. This requires your working tree to be clean (no modifications from the HEAD commit). + +Note: git revert is used to record some new commits to reverse the effect of some earlier commits (often only a faulty one). If you want to throw away all uncommitted changes in your working directory, you should see reset, particularly the `hard` option. Take care with these alternatives as they will discard uncommitted changes in your working directory. + +Returns a Commit representing the new `HEAD`. + +== Options + +commits:: (`List`, default: `[]]`) Commits to revert. For a more complete list of ways to spell commit names, see resolve (specifically the `toCommit` method). + +== Examples + +[source, groovy] +---- +git.revert(commits: ['1234567', '1234568']) +---- + +== See Also + +- link:https://git-scm.com/docs/git-revert[git-revert] diff --git a/groovy-git/src/docs/asciidoc/show.adoc b/groovy-git/src/docs/asciidoc/show.adoc new file mode 100644 index 0000000..b5678bd --- /dev/null +++ b/groovy-git/src/docs/asciidoc/show.adoc @@ -0,0 +1,40 @@ += show + +== Name + +show - Show a commit + +== Synopsis + +[source, groovy] +---- +git.show() +---- + +[source, groovy] +---- +git.show(commit: ) +---- + +[source, groovy] +---- +git.show { + commit = +} +---- + +== Description + +Shows a commit including it's message and diff. + +Returns a CommitDiff for the given commit. + +== Options + +commit:: (`Object`, default: `HEAD`) A revstring-ish object naming the commit to be shown. Commit-ish object names to include the history of in the output. For a more complete list of ways to specify a revstring, see resolve (specifically the `toRevisionString` method). + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-show[git-show] diff --git a/groovy-git/src/docs/asciidoc/status.adoc b/groovy-git/src/docs/asciidoc/status.adoc new file mode 100644 index 0000000..a9b51a1 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/status.adoc @@ -0,0 +1,28 @@ += status + +== Name + +status - Show the working tree status + +== Synopsis + +[source, groovy] +---- +git.status() +---- + +== Description + +Displays paths that have differences between the index file and the current `HEAD` commit, paths that have differences between the working tree and the index file, and paths in the working tree that are not tracked by Git (and are not ignored by gitignore). The first are what you would commit by running `commit`; the second and third are what you could commit by running `add` before running `commit`. + +Returns a Status instance detailing the paths that differ. + +== Options + +_None_ + +== Examples + +== See Also + +- link:https://git-scm.com/docs/git-status[git-status] diff --git a/groovy-git/src/docs/asciidoc/tag.adoc b/groovy-git/src/docs/asciidoc/tag.adoc new file mode 100644 index 0000000..a88b323 --- /dev/null +++ b/groovy-git/src/docs/asciidoc/tag.adoc @@ -0,0 +1,96 @@ += tag + +== Name + +tag - Create, list, or delete tag object + +== Synopsis + +[source, groovy] +---- +git.tag.list() +---- + +[source, groovy] +---- +git.tag.add(name: , pointsTo: , force: , + annotate: , message: , tagger: ) +---- + +[source, groovy] +---- +git.tag.remove(names: [, ...]) +---- + +== Description + + +`git.tag.list()`:: Returns a list of tags (Tag). +`git.tag.add(name: , pointsTo: , force: , annotate: , message: , tagger: )`:: Creates a new tag named `` pointing at ``. +Returns the created Tag. +`git.tag.remove(names: [, ...])`:: Removes one or more tages. Returns a `List` of tag names removed. + +== Options + +=== add + +name:: (`String`, default `null`) Name of the tag +message:: (`String`, default `null`) Use the given as the commit message. +tagger:: (`Person`, default `null`) Override the tagger recorded in the tag. This must be a Person. +annotate:: (`boolean`, default `true`) Make an unsigned, annotated tag object +force:: (`boolean`, default `false`) Replace an existing tag with the given name (instead of failing) +pointsTo:: (`Object`, default `null`) Point new tag at this commit. For a more complete list of acceptable inputs, see resolve (specifically the `toRevisionString` method). + +=== remove + +names:: (`List`, default `[]`) Names of the tags. For a more complete list of acceptable inputs, see resolve (specifically the `toTagName` method). + +== Examples + +To list all tags. + +[source, groovy] +---- +def tags = git.tag.list() +---- + +Add an annotated tag. + +[source, groovy] +---- +git.tag.add(name: 'new-tag') +git.tag.add(name: 'new-tag', message: 'Some message') +git.tag.add(name: 'new-tag', annotate: true) +---- + +Add an unannotated tag. + +[source, groovy] +---- +git.tag.add(name: 'new-tag', annotate: false) +---- + +Add a tag starting at a specific commit. + +[source, groovy] +---- +git.tag.add(name: 'new-tag', pointsTo: 'other-branch') +---- + +Overwrite an existing tag. + +[source, groovy] +---- +git.tag.add(name: 'existing-tag', force: true) +---- + +Remove tags. + +[source, groovy] +---- +def removedTags = git.tag.remove(names: ['the-tag']) +---- + +== See Also + +- link:https://git-scm.com/docs/git-tag[git-tag] diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Branch.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Branch.groovy new file mode 100644 index 0000000..f3786c2 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Branch.groovy @@ -0,0 +1,30 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable + +import org.eclipse.jgit.lib.Repository + +/** + * A branch. + */ +@Immutable +class Branch { + /** + * The fully qualified name of this branch. + */ + String fullName + + /** + * This branch's upstream branch. {@code null} if this branch isn't + * tracking an upstream. + */ + Branch trackingBranch + + /** + * The simple name of the branch. + * @return the simple name + */ + String getName() { + return Repository.shortenRefName(fullName) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/BranchStatus.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/BranchStatus.groovy new file mode 100644 index 0000000..c1d4d55 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/BranchStatus.groovy @@ -0,0 +1,24 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable + +/** + * The tracking status of a branch. + */ +@Immutable +class BranchStatus { + /** + * The branch this object is for. + */ + Branch branch + + /** + * The number of commits this branch is ahead of its upstream. + */ + int aheadCount + + /** + * The number of commits this branch is behind its upstream. + */ + int behindCount +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Commit.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Commit.groovy new file mode 100644 index 0000000..b1adcbc --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Commit.groovy @@ -0,0 +1,51 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable +import java.time.ZonedDateTime + +/** + * A commit. + */ +@Immutable(knownImmutableClasses=[ZonedDateTime]) +class Commit { + /** + * The full hash of the commit. + */ + String id + + /** + * The abbreviated hash of the commit. + */ + String abbreviatedId + + /** + * Hashes of any parent commits. + */ + List parentIds + + /** + * The author of the changes in the commit. + */ + Person author + + /** + * The committer of the changes in the commit. + */ + Person committer + + /** + * The time the commit was created with the time zone of the committer, if available. + */ + ZonedDateTime dateTime + + /** + * The full commit message. + */ + String fullMessage + + /** + * The shortened commit message. + */ + String shortMessage + +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/CommitDiff.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/CommitDiff.groovy new file mode 100644 index 0000000..5ca6ad3 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/CommitDiff.groovy @@ -0,0 +1,28 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable +import groovy.transform.ToString + +@Immutable +@ToString(includeNames=true) +class CommitDiff { + Commit commit + + Set added = [] + + Set copied = [] + + Set modified = [] + + Set removed = [] + + Set renamed = [] + + /** + * Gets all changed files. + * @return all changed files + */ + Set getAllChanges() { + return added + copied + modified + removed + renamed + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Configurable.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Configurable.groovy new file mode 100644 index 0000000..141e1cf --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Configurable.groovy @@ -0,0 +1,9 @@ +package org.xbib.groovy.git + +import org.xbib.groovy.git.internal.AnnotateAtRuntime + +@FunctionalInterface +@AnnotateAtRuntime(annotations = "org.gradle.api.HasImplicitReceiver") +interface Configurable { + void configure(T t) +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Credentials.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Credentials.groovy new file mode 100644 index 0000000..f4cc7df --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Credentials.groovy @@ -0,0 +1,35 @@ +package org.xbib.groovy.git + +import groovy.transform.Canonical + +/** + * Credentials to use for remote operations. + */ +@Canonical +class Credentials { + + final String username + + final String password + + Credentials() { + this(null, null) + } + + Credentials(String username, String password) { + this.username = username + this.password = password + } + + String getUsername() { + return username ?: '' + } + + String getPassword() { + return password ?: '' + } + + boolean isPopulated() { + return username != null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Git.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Git.groovy new file mode 100644 index 0000000..cfade76 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Git.groovy @@ -0,0 +1,182 @@ +package org.xbib.groovy.git + +import org.xbib.groovy.git.internal.WithOperations +import org.xbib.groovy.git.operation.AddOp +import org.xbib.groovy.git.operation.ApplyOp +import org.xbib.groovy.git.operation.CheckoutOp +import org.xbib.groovy.git.operation.CleanOp +import org.xbib.groovy.git.operation.CloneOp +import org.xbib.groovy.git.operation.CommitOp +import org.xbib.groovy.git.operation.DescribeOp +import org.xbib.groovy.git.operation.FetchOp +import org.xbib.groovy.git.operation.InitOp +import org.xbib.groovy.git.operation.LogOp +import org.xbib.groovy.git.operation.LsRemoteOp +import org.xbib.groovy.git.operation.MergeOp +import org.xbib.groovy.git.operation.OpenOp +import org.xbib.groovy.git.operation.PullOp +import org.xbib.groovy.git.operation.PushOp +import org.xbib.groovy.git.operation.ResetOp +import org.xbib.groovy.git.operation.RevertOp +import org.xbib.groovy.git.operation.RmOp +import org.xbib.groovy.git.operation.ShowOp +import org.xbib.groovy.git.operation.StatusOp +import org.xbib.groovy.git.service.BranchService +import org.xbib.groovy.git.service.RemoteService +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.service.TagService +import org.xbib.groovy.git.util.GitUtil + +/** + * Provides support for performing operations on and getting information about + * a Git repository. + * + *

A Git instance can be obtained via 3 methods.

+ * + *
    + *
  • + *

    {@link org.xbib.groovy.git.operation.OpenOp Open} an existing repository.

    + *
    def git = Git.open(dir: 'path/to/my/repo')
    + *
  • + *
  • + *

    {@link org.xbib.groovy.git.operation.InitOp Initialize} a new repository.

    + *
    def git = Git.init(dir: 'path/to/my/repo')
    + *
  • + *
  • + *

    {@link org.xbib.groovy.git.operation.CloneOp Clone} an existing repository.

    + *
    def git = Git.clone(dir: 'path/to/my/repo', uri: 'git@github.com:jprante/groovy-git.git')
    + *
  • + *
+ * + *

+ * Once obtained, operations can be called with two syntaxes. + *

+ * + *
    + *
  • + *

    Map syntax. Any public property on the {@code *Op} class can be provided as a Map entry.

    + *
    git.commit(message: 'Committing my code.', amend: true)
    + *
  • + *
  • + *

    Closure syntax. Any public property or method on the {@code *Op} class can be used.

    + *
    + * git.log {
    + *   range 'master', 'my-new-branch'
    + *   maxCommits = 5
    + * }
    + *	 
    + *
  • + *
+ * + *

+ * Details of each operation's properties and methods are available on the + * doc page for the class. The following operations are supported directly on a + * Git instance. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.operation.AddOp add}
  • + *
  • {@link org.xbib.groovy.git.operation.ApplyOp apply}
  • + *
  • {@link org.xbib.groovy.git.operation.CheckoutOp checkout}
  • + *
  • {@link org.xbib.groovy.git.operation.CleanOp clean}
  • + *
  • {@link org.xbib.groovy.git.operation.CommitOp commit}
  • + *
  • {@link org.xbib.groovy.git.operation.DescribeOp describe}
  • + *
  • {@link org.xbib.groovy.git.operation.FetchOp fetch}
  • + *
  • {@link org.xbib.groovy.git.operation.LogOp log}
  • + *
  • {@link org.xbib.groovy.git.operation.LsRemoteOp lsremote}
  • + *
  • {@link org.xbib.groovy.git.operation.MergeOp merge}
  • + *
  • {@link org.xbib.groovy.git.operation.PullOp pull}
  • + *
  • {@link org.xbib.groovy.git.operation.PushOp push}
  • + *
  • {@link org.xbib.groovy.git.operation.RmOp remove}
  • + *
  • {@link org.xbib.groovy.git.operation.ResetOp reset}
  • + *
  • {@link org.xbib.groovy.git.operation.RevertOp revert}
  • + *
  • {@link org.xbib.groovy.git.operation.ShowOp show}
  • + *
  • {@link org.xbib.groovy.git.operation.StatusOp status}
  • + *
+ * + *

+ * And the following operations are supported statically on the Git class. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.operation.CloneOp clone}
  • + *
  • {@link org.xbib.groovy.git.operation.InitOp init}
  • + *
  • {@link org.xbib.groovy.git.operation.OpenOp open}
  • + *
+ * + *

+ * Further operations are available on the following services. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.service.BranchService branch}
  • + *
  • {@link org.xbib.groovy.git.service.RemoteService remote}
  • + *
  • {@link org.xbib.groovy.git.service.ResolveService resolve}
  • + *
  • {@link org.xbib.groovy.git.service.TagService tag}
  • + *
+ */ +@WithOperations(staticOperations=[InitOp, CloneOp, OpenOp], instanceOperations=[CleanOp, + StatusOp, AddOp, RmOp, ResetOp, ApplyOp, PullOp, PushOp, FetchOp, LsRemoteOp, + CheckoutOp, LogOp, CommitOp, RevertOp, MergeOp, DescribeOp, ShowOp]) +class Git implements AutoCloseable { + /** + * The repository opened by this object. + */ + final Repository repository + + /** + * Supports operations on branches. + */ + final BranchService branch + + /** + * Supports operations on remotes. + */ + final RemoteService remote + + /** + * Convenience methods for resolving various objects. + */ + final ResolveService resolve + + /** + * Supports operations on tags. + */ + final TagService tag + + Git(Repository repository) { + this.repository = repository + this.branch = new BranchService(repository) + this.remote = new RemoteService(repository) + this.tag = new TagService(repository) + this.resolve = new ResolveService(repository) + } + + /** + * Returns the commit located at the current HEAD of the repository. + * @return the current HEAD commit + */ + Commit head() { + return resolve.toCommit('HEAD') + } + + /** + * Checks if {@code base} is an ancestor of {@code tip}. + * @param base the version that might be an ancestor + * @param tip the tip version + */ + boolean isAncestorOf(Object base, Object tip) { + Commit baseCommit = resolve.toCommit(base) + Commit tipCommit = resolve.toCommit(tip) + return GitUtil.isAncestorOf(repository, baseCommit, tipCommit) + } + + /** + * Release underlying resources used by this instance. After calling close + * you should not use this instance anymore. + */ + @Override + void close() { + repository.jgit.close() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Person.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Person.groovy new file mode 100644 index 0000000..6fce877 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Person.groovy @@ -0,0 +1,19 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable + +/** + * A person. + */ +@Immutable +class Person { + /** + * Name of person. + */ + String name + + /** + * Email address of person. + */ + String email +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/PushException.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/PushException.groovy new file mode 100644 index 0000000..5969057 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/PushException.groovy @@ -0,0 +1,14 @@ +package org.xbib.groovy.git + +import org.eclipse.jgit.api.errors.TransportException + +class PushException extends TransportException { + + PushException(String message) { + super(message) + } + + PushException(String message, Throwable cause) { + super(message, cause) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Ref.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Ref.groovy new file mode 100644 index 0000000..7a1a2be --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Ref.groovy @@ -0,0 +1,24 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable + +import org.eclipse.jgit.lib.Repository + +/** + * A ref. + */ +@Immutable +class Ref { + /** + * The fully qualified name of this ref. + */ + String fullName + + /** + * The simple name of the ref. + * @return the simple name + */ + String getName() { + return Repository.shortenRefName(fullName) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Remote.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Remote.groovy new file mode 100644 index 0000000..d626d5c --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Remote.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable + +/** + * Remote repository. + */ +@Immutable +class Remote { + /** + * Name of the remote. + */ + String name + + /** + * URL to fetch from. + */ + String url + + /** + * URL to push to. + */ + String pushUrl + + /** + * Specs to fetch from the remote. + */ + List fetchRefSpecs = [] + + /** + * Specs to push to the remote. + */ + List pushRefSpecs = [] + + /** + * Whether or not pushes will mirror the repository. + */ + boolean mirror +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Repository.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Repository.groovy new file mode 100644 index 0000000..c6ef0ad --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Repository.groovy @@ -0,0 +1,34 @@ +package org.xbib.groovy.git + +import org.eclipse.jgit.api.Git + +/** + * A repository. + */ +class Repository { + /** + * The directory the repository is contained in. + */ + File rootDir + + /** + * The JGit instance opened for this repository. + */ + Git jgit + + /** + * The credentials used when talking to remote repositories. + */ + Credentials credentials + + Repository(File rootDir, Git jgit, Credentials credentials) { + this.rootDir = rootDir + this.jgit = jgit + this.credentials = credentials + } + + @Override + String toString() { + return "Repository(${rootDir.canonicalPath})" + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Status.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Status.groovy new file mode 100644 index 0000000..445f4d9 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Status.groovy @@ -0,0 +1,60 @@ +package org.xbib.groovy.git + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Status of the current working tree and index. + */ +@EqualsAndHashCode +@ToString(includeNames=true) +class Status { + final Changes staged + final Changes unstaged + final Set conflicts + + Status(Map args = [:]) { + def invalidArgs = args.keySet() - ['staged', 'unstaged', 'conflicts'] + if (invalidArgs) { + throw new IllegalArgumentException("Following keys are not supported: ${invalidArgs}") + } + this.staged = 'staged' in args ? new Changes(args.staged) : new Changes() + this.unstaged = 'unstaged' in args ? new Changes(args.unstaged) : new Changes() + this.conflicts = 'conflicts' in args ? args.conflicts : [] + } + + @EqualsAndHashCode + @ToString(includeNames=true) + class Changes { + final Set added + final Set modified + final Set removed + + Changes(Map args = [:]) { + def invalidArgs = args.keySet() - ['added', 'modified', 'removed'] + if (invalidArgs) { + throw new IllegalArgumentException("Following keys are not supported: ${invalidArgs}") + } + this.added = 'added' in args ? args.added : [] + this.modified = 'modified' in args ? args.modified : [] + this.removed = 'removed' in args ? args.removed : [] + } + + /** + * Gets all changed files. + * @return all changed files + */ + Set getAllChanges() { + return added + modified + removed + } + } + + /** + * Whether the repository has any changes or conflicts. + * @return {@code true} if there are no changes either staged or unstaged or + * any conflicts, {@code false} otherwise + */ + boolean isClean() { + return (staged.allChanges + unstaged.allChanges + conflicts).empty + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/Tag.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/Tag.groovy new file mode 100644 index 0000000..45eaab7 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/Tag.groovy @@ -0,0 +1,49 @@ +package org.xbib.groovy.git + +import groovy.transform.Immutable +import java.time.ZonedDateTime +import org.eclipse.jgit.lib.Repository + +/** + * A tag. + */ +@Immutable(knownImmutableClasses=[ZonedDateTime]) +class Tag { + /** + * The commit this tag points to. + */ + Commit commit + + /** + * The person who created the tag. + */ + Person tagger + + /** + * The full name of this tag. + */ + String fullName + + /** + * The full tag message. + */ + String fullMessage + + /** + * The shortened tag message. + */ + String shortMessage + + /** + * The time the commit was created with the time zone of the committer, if available. + */ + ZonedDateTime dateTime + + /** + * The simple name of this tag. + * @return the simple name + */ + String getName() { + return Repository.shortenRefName(fullName) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/AuthConfig.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/AuthConfig.groovy new file mode 100644 index 0000000..730d045 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/AuthConfig.groovy @@ -0,0 +1,57 @@ +package org.xbib.groovy.git.auth + +import org.xbib.groovy.git.Credentials + +class AuthConfig { + + static final String USERNAME_OPTION = 'org.xbib.groovy.git.auth.username' + static final String PASSWORD_OPTION = 'org.xbib.groovy.git.auth.password' + + static final String USERNAME_ENV_VAR = 'GROOVY_GIT_USER' + static final String PASSWORD_ENV_VAR = 'GROOVY_GIT_PASS' + + private final Map props + private final Map env + + + private AuthConfig(Map props, Map env) { + this.props = props + this.env = env + + GitSystemReader.install() + } + + /** + * Constructs and returns a {@link Credentials} instance reflecting the + * settings in the system properties. + * @return a credentials instance reflecting the settings in the system + * properties, or, if the username isn't set, {@code null} + */ + Credentials getHardcodedCreds() { + String username = props[USERNAME_OPTION] ?: env[USERNAME_ENV_VAR] + String password = props[PASSWORD_OPTION] ?: env[PASSWORD_ENV_VAR] + return new Credentials(username, password) + } + + /** + * Factory method to construct an authentication configuration from the + * given properties and environment. + * @param properties the properties to use in this configuration + * @param env the environment vars to use in this configuration + * @return the constructed configuration + * @throws IllegalArgumentException if force is set to an invalid option + */ + static AuthConfig fromMap(Map props, Map env = [:]) { + return new AuthConfig(props, env) + } + + /** + * Factory method to construct an authentication configuration from the + * current system properties and environment variables. + * @return the constructed configuration + * @throws IllegalArgumentException if force is set to an invalid option + */ + static AuthConfig fromSystem() { + return fromMap(System.properties, System.env) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/GitSystemReader.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/GitSystemReader.java new file mode 100644 index 0000000..b459507 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/GitSystemReader.java @@ -0,0 +1,173 @@ +package org.xbib.groovy.git.auth; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.TimeZone; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.SystemReader; +import org.eclipse.jgit.util.time.MonotonicClock; + +public class GitSystemReader extends SystemReader { + + private static final Pattern PATH_SPLITTER = Pattern.compile(Pattern.quote(File.pathSeparator)); + + private final SystemReader delegate; + + private final String gitSsh; + + public GitSystemReader(SystemReader delegate, String gitSsh) { + this.delegate = delegate; + this.gitSsh = gitSsh; + } + + @Override + public String getHostname() { + return delegate.getHostname(); + } + + @Override + public String getenv(String variable) { + String value = delegate.getenv(variable); + if ("GIT_SSH".equals(variable) && value == null) { + return gitSsh; + } else { + return value; + } + } + + @Override + public String getProperty(String key) { + return delegate.getProperty(key); + } + + @Override + public FileBasedConfig openUserConfig(Config parent, FS fs) { + return delegate.openUserConfig(parent, fs); + } + + @Override + public FileBasedConfig openSystemConfig(Config parent, FS fs) { + return delegate.openSystemConfig(parent, fs); + } + + @Override + public FileBasedConfig openJGitConfig(Config parent, FS fs) { + return delegate.openJGitConfig(parent, fs); + } + + @Override + public long getCurrentTime() { + return delegate.getCurrentTime(); + } + + @Override + public MonotonicClock getClock() { + return delegate.getClock(); + } + + @Override + public int getTimezone(long when) { + return delegate.getTimezone(when); + } + + @Override + public TimeZone getTimeZone() { + return delegate.getTimeZone(); + } + + @Override + public Locale getLocale() { + return delegate.getLocale(); + } + + @Override + public SimpleDateFormat getSimpleDateFormat(String pattern) { + return delegate.getSimpleDateFormat(pattern); + } + + @Override + public SimpleDateFormat getSimpleDateFormat(String pattern, Locale locale) { + return delegate.getSimpleDateFormat(pattern, locale); + } + + @Override + public DateFormat getDateTimeInstance(int dateStyle, int timeStyle) { + return delegate.getDateTimeInstance(dateStyle, timeStyle); + } + + @Override + public boolean isWindows() { + return delegate.isWindows(); + } + + @Override + public boolean isMacOS() { + return delegate.isWindows(); + } + + @Override + public void checkPath(String path) throws CorruptObjectException { + delegate.checkPath(path); + } + + @Override + public void checkPath(byte[] path) throws CorruptObjectException { + delegate.checkPath(path); + } + + public static void install() { + SystemReader current = SystemReader.getInstance(); + + String gitSsh = Stream.of("ssh") + .map(GitSystemReader::findExecutable) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElse(null); + + SystemReader grgit = new GitSystemReader(current, gitSsh); + SystemReader.setInstance(grgit); + } + + private static Optional findExecutable(String exe) { + List extensions = Optional.ofNullable(System.getenv("PATHEXT")) + .map(PATH_SPLITTER::splitAsStream) + .map(stream -> stream.collect(Collectors.toList())) + .orElse(Collections.emptyList()); + + Function> getCandidatePaths = dir -> { + // assume PATHEXT is only set on Windows + if (extensions.isEmpty()) { + return Stream.of(dir.resolve(exe)); + } else { + return extensions.stream() + .map(ext -> dir.resolve(exe + ext)); + } + }; + + return PATH_SPLITTER.splitAsStream(System.getenv("PATH")) + .map(Paths::get) + .flatMap(getCandidatePaths) + .filter(Files::isExecutable) + .map(Path::toAbsolutePath) + .map(Path::toString) + .findFirst(); + } +} + diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/TransportOpUtil.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/TransportOpUtil.groovy new file mode 100644 index 0000000..04ac6c5 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/auth/TransportOpUtil.groovy @@ -0,0 +1,34 @@ +package org.xbib.groovy.git.auth + +import org.xbib.groovy.git.Credentials + +import org.eclipse.jgit.api.TransportCommand +import org.eclipse.jgit.transport.CredentialsProvider +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider + +final class TransportOpUtil { + + private TransportOpUtil() { + } + + /** + * Configures the given transport command with the given credentials. + * @param cmd the command to configure + * @param credentials the hardcoded credentials to use, if not {@code null} + */ + static void configure(TransportCommand cmd, Credentials credentials) { + AuthConfig config = AuthConfig.fromSystem() + cmd.setCredentialsProvider(determineCredentialsProvider(config, credentials)) + } + + private static CredentialsProvider determineCredentialsProvider(AuthConfig config, Credentials credentials) { + Credentials systemCreds = config.hardcodedCreds + if (credentials?.populated) { + return new UsernamePasswordCredentialsProvider(credentials.username, credentials.password) + } else if (systemCreds?.populated) { + return new UsernamePasswordCredentialsProvider(systemCreds.username, systemCreds.password) + } else { + return null + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntime.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntime.java new file mode 100644 index 0000000..bf74e20 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntime.java @@ -0,0 +1,15 @@ +package org.xbib.groovy.git.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +@GroovyASTTransformationClass("org.xbib.groovy.git.internal.AnnotateAtRuntimeASTTransformation") +public @interface AnnotateAtRuntime { + String[] annotations() default {}; +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntimeASTTransformation.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntimeASTTransformation.java new file mode 100644 index 0000000..7976bc4 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/AnnotateAtRuntimeASTTransformation.java @@ -0,0 +1,34 @@ +package org.xbib.groovy.git.internal; + +import java.util.List; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.AbstractASTTransformation; +import org.codehaus.groovy.transform.GroovyASTTransformation; + +@GroovyASTTransformation +public final class AnnotateAtRuntimeASTTransformation extends AbstractASTTransformation { + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + AnnotationNode annotation = (AnnotationNode) nodes[0]; + AnnotatedNode parent = (AnnotatedNode) nodes[1]; + + ClassNode clazz = (ClassNode) parent; + List annotations = getMemberList(annotation, "annotations"); + for (String name : annotations) { + // !!! UGLY HACK !!! + // Groovy won't think the class is an annotation when creating a ClassNode just based on the name. + // Instead, we create a node based on an interface and then overwrite the name to get the interface + // we actually want. + ClassNode base = new ClassNode(FunctionalInterface.class); + base.setName(name); + + clazz.addAnnotation(new AnnotationNode(base)); + } + } +} + diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/OpSyntax.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/OpSyntax.groovy new file mode 100644 index 0000000..547997a --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/OpSyntax.groovy @@ -0,0 +1,40 @@ +package org.xbib.groovy.git.internal + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Configurable + +class OpSyntax { + static def noArgOperation(Class opClass, Object[] classArgs) { + def op = opClass.newInstance(classArgs) + return op.call() + } + + static def mapOperation(Class opClass, Object[] classArgs, Map args) { + def op = opClass.newInstance(classArgs) + + args.forEach { key, value -> + op[key] = value + } + + return op.call() + } + + static def samOperation(Class opClass, Object[] classArgs, Configurable arg) { + def op = opClass.newInstance(classArgs) + arg.configure(op) + return op.call() + } + + static def closureOperation(Class opClass, Object[] classArgs, Closure closure) { + def op = opClass.newInstance(classArgs) + + Object originalDelegate = closure.delegate + closure.delegate = op + closure.resolveStrategy = Closure.DELEGATE_FIRST + closure.call() + closure.delegate = originalDelegate + + return op.call() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/Operation.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/Operation.java new file mode 100644 index 0000000..260e4dc --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/Operation.java @@ -0,0 +1,12 @@ +package org.xbib.groovy.git.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Operation { + String value(); +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperations.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperations.java new file mode 100644 index 0000000..1ecde51 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperations.java @@ -0,0 +1,18 @@ +package org.xbib.groovy.git.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.Callable; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +@GroovyASTTransformationClass("org.xbib.groovy.git.internal.WithOperationsASTTransformation") +public @interface WithOperations { + Class>[] staticOperations() default {}; + + Class>[] instanceOperations() default {}; +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperationsASTTransformation.java b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperationsASTTransformation.java new file mode 100644 index 0000000..b91d492 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/internal/WithOperationsASTTransformation.java @@ -0,0 +1,180 @@ +package org.xbib.groovy.git.internal; + +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import groovy.lang.Closure; +import org.xbib.groovy.git.Configurable; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.ArrayExpression; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.AbstractASTTransformation; +import org.codehaus.groovy.transform.GroovyASTTransformation; + +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class WithOperationsASTTransformation extends AbstractASTTransformation { + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + AnnotationNode annotation = (AnnotationNode) nodes[0]; + AnnotatedNode parent = (AnnotatedNode) nodes[1]; + + if (parent instanceof ClassNode) { + ClassNode clazz = (ClassNode) parent; + List staticOps = getClassList(annotation, "staticOperations"); + List instanceOps = getClassList(annotation, "instanceOperations"); + + staticOps.forEach(op -> makeMethods(clazz, op, true)); + instanceOps.forEach(op -> makeMethods(clazz, op, false)); + } + } + + private void makeMethods(ClassNode targetClass, ClassNode opClass, boolean isStatic) { + AnnotationNode annotation = opClass.getAnnotations(classFromType(Operation.class)).stream() + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Class is not annotated with @Operation: " + opClass)); + String opName = getMemberStringValue(annotation, "value"); + ClassNode opReturn = opClass.getDeclaredMethod("call", new Parameter[] {}).getReturnType(); + + targetClass.addMethod(makeNoArgMethod(targetClass, opName, opClass, opReturn, isStatic)); + targetClass.addMethod(makeMapMethod(targetClass, opName, opClass, opReturn, isStatic)); + targetClass.addMethod(makeSamMethod(targetClass, opName, opClass, opReturn, isStatic)); + targetClass.addMethod(makeClosureMethod(targetClass, opName, opClass, opReturn, isStatic)); + } + + private MethodNode makeNoArgMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { + Parameter[] parms = new Parameter[] {}; + + Statement code = new ExpressionStatement( + new StaticMethodCallExpression( + classFromType(OpSyntax.class), + "noArgOperation", + new ArgumentListExpression( + new ClassExpression(opClass), + new ArrayExpression( + classFromType(Object.class), + opConstructorParms(targetClass, isStatic))))); + + return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); + } + + private MethodNode makeMapMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { + ClassNode parmType = classFromType(Map.class); + GenericsType[] generics = genericsFromTypes(String.class, Object.class); + parmType.setGenericsTypes(generics); + Parameter[] parms = new Parameter[] {new Parameter(parmType, "args")}; + + Statement code = new ExpressionStatement( + new StaticMethodCallExpression( + classFromType(OpSyntax.class), + "mapOperation", + new ArgumentListExpression( + new ClassExpression(opClass), + new ArrayExpression( + classFromType(Object.class), + opConstructorParms(targetClass, isStatic)), + new VariableExpression("args")))); + + return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); + } + + private MethodNode makeSamMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { + ClassNode parmType = classFromType(Configurable.class); + GenericsType[] generics = new GenericsType[] {new GenericsType(opClass)}; + parmType.setGenericsTypes(generics); + Parameter[] parms = new Parameter[] {new Parameter(parmType, "arg")}; + + Statement code = new ExpressionStatement( + new StaticMethodCallExpression( + classFromType(OpSyntax.class), + "samOperation", + new ArgumentListExpression( + new ClassExpression(opClass), + new ArrayExpression( + classFromType(Object.class), opConstructorParms(targetClass, isStatic)), + new VariableExpression("arg")))); + + return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); + } + + private MethodNode makeClosureMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { + ClassNode parmType = classFromType(Closure.class); + Parameter[] parms = new Parameter[] {new Parameter(parmType, "arg")}; + + Statement code = new ExpressionStatement( + new StaticMethodCallExpression( + classFromType(OpSyntax.class), + "closureOperation", + new ArgumentListExpression( + new ClassExpression(opClass), + new ArrayExpression( + classFromType(Object.class), + opConstructorParms(targetClass, isStatic)), + new VariableExpression("arg")))); + + return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); + } + + public ClassNode classFromType(Type type) { + if (type instanceof Class) { + Class clazz = (Class) type; + if (clazz.isPrimitive()) { + return ClassHelper.make(clazz); + } else { + return ClassHelper.makeWithoutCaching(clazz, false); + } + } else if (type instanceof ParameterizedType) { + ParameterizedType ptype = (ParameterizedType) type; + ClassNode base = classFromType(ptype.getRawType()); + GenericsType[] generics = genericsFromTypes(ptype.getActualTypeArguments()); + base.setGenericsTypes(generics); + return base; + } else { + throw new IllegalArgumentException("Unsupported type: " + type.getClass()); + } + } + + public GenericsType[] genericsFromTypes(Type... types) { + return Arrays.stream(types) + .map(this::classFromType) + .map(GenericsType::new) + .toArray(GenericsType[]::new); + } + + public List opConstructorParms(ClassNode targetClass, boolean isStatic) { + if (isStatic) { + return Collections.emptyList(); + } else { + return Collections.singletonList(new FieldExpression(targetClass.getField("repository"))); + } + } + + public int modifiers(boolean isStatic) { + int modifiers = Modifier.PUBLIC | Modifier.FINAL; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + return modifiers; + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/AddOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/AddOp.groovy new file mode 100644 index 0000000..bcdab08 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/AddOp.groovy @@ -0,0 +1,38 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.AddCommand + +/** + * Adds files to the index. + */ +@Operation('add') +class AddOp implements Callable { + private final Repository repo + + /** + * Patterns of files to add to the index. + */ + Set patterns = [] + + /** + * {@code true} if changes to all currently tracked files should be added + * to the index, {@code false} otherwise. + */ + boolean update = false + + AddOp(Repository repo) { + this.repo = repo + } + + Void call() { + AddCommand cmd = repo.jgit.add() + patterns.each { cmd.addFilepattern(it) } + cmd.update = update + cmd.call() + return null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ApplyOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ApplyOp.groovy new file mode 100644 index 0000000..e0845c8 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ApplyOp.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.CoercionUtil +import org.eclipse.jgit.api.ApplyCommand + +/** + * Apply a patch to the index. + */ +@Operation('apply') +class ApplyOp implements Callable { + private final Repository repo + + /** + * The patch file to apply to the index. + * @see {@link CoercionUtil#toFile(Object)} + */ + Object patch + + ApplyOp(Repository repo) { + this.repo = repo + } + + @Override + Void call() { + ApplyCommand cmd = repo.jgit.apply() + if (!patch) { + throw new IllegalStateException('Must set a patch file.') + } + CoercionUtil.toFile(patch).withInputStream { stream -> + cmd.patch = stream + cmd.call() + } + return + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchAddOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchAddOp.groovy new file mode 100644 index 0000000..89e1778 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchAddOp.groovy @@ -0,0 +1,70 @@ +package org.xbib.groovy.git.operation + +import org.eclipse.jgit.api.CreateBranchCommand +import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode +import org.eclipse.jgit.lib.Ref +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil + +import java.util.concurrent.Callable + +/** + * Adds a branch to the repository. Returns the newly created {@link Branch}. + */ +@Operation('add') +class BranchAddOp implements Callable { + private final Repository repo + + /** + * The name of the branch to add. + */ + String name + + /** + * The commit the branch should start at. If this is a remote branch + * it will be automatically tracked. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object startPoint + + /** + * The tracking mode to use. If {@code null}, will use the default + * behavior. + */ + Mode mode + + BranchAddOp(Repository repo) { + this.repo = repo + } + + Branch call() { + if (mode && !startPoint) { + throw new IllegalStateException('Cannot set mode if no start point.') + } + CreateBranchCommand cmd = repo.jgit.branchCreate() + cmd.name = name + cmd.force = false + if (startPoint) { + String rev = new ResolveService(repo).toRevisionString(startPoint) + cmd.startPoint = rev + } + if (mode) { cmd.upstreamMode = mode.jgit } + + Ref ref = cmd.call() + return GitUtil.resolveBranch(repo, ref) + } + + static enum Mode { + TRACK(SetupUpstreamMode.TRACK), + NO_TRACK(SetupUpstreamMode.NOTRACK) + + private final SetupUpstreamMode jgit + + Mode(SetupUpstreamMode jgit) { + this.jgit = jgit + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchChangeOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchChangeOp.groovy new file mode 100644 index 0000000..0d61677 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchChangeOp.groovy @@ -0,0 +1,71 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.CreateBranchCommand +import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode +import org.eclipse.jgit.lib.Ref + +/** + * Changes a branch's start point and/or upstream branch. Returns the changed {@link Branch}. + */ +@Operation('change') +class BranchChangeOp implements Callable { + private final Repository repo + + /** + * The name of the branch to change. + */ + String name + + /** + * The commit the branch should now start at. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object startPoint + + /** + * The tracking mode to use. + */ + Mode mode + + BranchChangeOp(Repository repo) { + this.repo = repo + } + + Branch call() { + if (!GitUtil.resolveBranch(repo, name)) { + throw new IllegalStateException("Branch does not exist: ${name}") + } + if (!startPoint) { + throw new IllegalArgumentException('Must set new startPoint.') + } + CreateBranchCommand cmd = repo.jgit.branchCreate() + cmd.name = name + cmd.force = true + if (startPoint) { + String rev = new ResolveService(repo).toRevisionString(startPoint) + cmd.startPoint = rev + } + if (mode) { cmd.upstreamMode = mode.jgit } + + Ref ref = cmd.call() + return GitUtil.resolveBranch(repo, ref) + } + + static enum Mode { + TRACK(SetupUpstreamMode.TRACK), + NO_TRACK(SetupUpstreamMode.NOTRACK) + + private final SetupUpstreamMode jgit + + Mode(SetupUpstreamMode jgit) { + this.jgit = jgit + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchListOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchListOp.groovy new file mode 100644 index 0000000..f616b54 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchListOp.groovy @@ -0,0 +1,55 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.ListBranchCommand + +/** + * Lists branches in the repository. Returns a list of {@link Branch}. + */ +@Operation('list') +class BranchListOp implements Callable> { + private final Repository repo + + /** + * Which branches to return. + */ + Mode mode = Mode.LOCAL + + /** + * Commit ref branches must contains + */ + Object contains = null + + BranchListOp(Repository repo) { + this.repo = repo + } + + List call() { + ListBranchCommand cmd = repo.jgit.branchList() + cmd.listMode = mode.jgit + if (contains) { + cmd.contains = new ResolveService(repo).toRevisionString(contains) + } + return cmd.call().collect { + GitUtil.resolveBranch(repo, it.name) + } + } + + static enum Mode { + ALL(ListBranchCommand.ListMode.ALL), + REMOTE(ListBranchCommand.ListMode.REMOTE), + LOCAL(null) + + private final ListBranchCommand.ListMode jgit + + private Mode(ListBranchCommand.ListMode jgit) { + this.jgit = jgit + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchRemoveOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchRemoveOp.groovy new file mode 100644 index 0000000..d4fc151 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchRemoveOp.groovy @@ -0,0 +1,41 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.api.DeleteBranchCommand + +/** + * Removes one or more branches from the repository. Returns a list of + * the fully qualified branch names that were removed. + */ +@Operation('remove') +class BranchRemoveOp implements Callable> { + private final Repository repo + + /** + * List of all branche names to remove. + * @see {@link ResolveService#toBranchName(Object)} + */ + List names = [] + + /** + * If {@code false} (the default), only remove branches that + * are merged into another branch. If {@code true} will delete + * regardless. + */ + boolean force = false + + BranchRemoveOp(Repository repo) { + this.repo = repo + } + + List call() { + DeleteBranchCommand cmd = repo.jgit.branchDelete() + cmd.setBranchNames(names.collect { new ResolveService(repo).toBranchName(it) } as String[]) + cmd.force = force + return cmd.call() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchStatusOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchStatusOp.groovy new file mode 100644 index 0000000..78710c0 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/BranchStatusOp.groovy @@ -0,0 +1,46 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.BranchStatus +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.lib.BranchTrackingStatus + +/** + * Gets the tracking status of a branch. Returns a {@link BranchStatus}. + * + *
+ * def status = git.branch.status(name: 'the-branch')
+ * 
+ */ +@Operation('status') +class BranchStatusOp implements Callable { + private final Repository repo + + /** + * The branch to get the status of. + * @see {@link ResolveService#toBranch(Object)} + */ + Object name + + BranchStatusOp(Repository repo) { + this.repo = repo + } + + BranchStatus call() { + Branch realBranch = new ResolveService(repo).toBranch(name) + if (realBranch.trackingBranch) { + BranchTrackingStatus status = BranchTrackingStatus.of(repo.jgit.repository, realBranch.fullName) + if (status) { + return new BranchStatus(realBranch, status.aheadCount, status.behindCount) + } else { + throw new IllegalStateException("Could not retrieve status for ${name}") + } + } else { + throw new IllegalStateException("${name} is not set to track another branch") + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CheckoutOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CheckoutOp.groovy new file mode 100644 index 0000000..99d9a3d --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CheckoutOp.groovy @@ -0,0 +1,62 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.api.CheckoutCommand + +/** + * Checks out a branch to the working tree. Does not support checking out + * specific paths. + */ +@Operation('checkout') +class CheckoutOp implements Callable { + private final Repository repo + + /** + * The branch or commit to checkout. + * @see {@link ResolveService#toBranchName(Object)} + */ + Object branch + + /** + * {@code true} if the branch does not exist and should be created, + * {@code false} (the default) otherwise + */ + boolean createBranch = false + + /** + * If {@code createBranch} or {@code orphan} is {@code true}, start the new branch + * at this commit. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object startPoint + + /** + * {@code true} if the new branch is to be an orphan, + * {@code false} (the default) otherwise + */ + boolean orphan = false + + CheckoutOp(Repository repo) { + this.repo = repo + } + + Void call() { + if (startPoint && !createBranch && !orphan) { + throw new IllegalArgumentException('cannot set a start point if createBranch and orphan are false') + } else if ((createBranch || orphan) && !branch) { + throw new IllegalArgumentException('must specify branch name to create') + } + CheckoutCommand cmd = repo.jgit.checkout() + ResolveService resolve = new ResolveService(repo) + if (branch) { cmd.name = resolve.toBranchName(branch) } + cmd.createBranch = createBranch + cmd.startPoint = resolve.toRevisionString(startPoint) + cmd.orphan = orphan + cmd.call() + return null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CleanOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CleanOp.groovy new file mode 100644 index 0000000..49230b5 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CleanOp.groovy @@ -0,0 +1,54 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.CleanCommand + +/** + * Remove untracked files from the working tree. Returns the list of + * file paths deleted. + */ +@Operation('clean') +class CleanOp implements Callable> { + private final Repository repo + + /** + * The paths to clean. {@code null} if all paths should be included. + */ + Set paths + + /** + * {@code true} if untracked directories should also be deleted, + * {@code false} (the default) otherwise + */ + boolean directories = false + + /** + * {@code true} if the files should be returned, but not deleted, + * {@code false} (the default) otherwise + */ + boolean dryRun = false + + /** + * {@code false} if files ignored by {@code .gitignore} should + * also be deleted, {@code true} (the default) otherwise + */ + boolean ignore = true + + CleanOp(Repository repo) { + this.repo = repo + } + + Set call() { + CleanCommand cmd = repo.jgit.clean() + if (paths) { + cmd.paths = paths + } + cmd.cleanDirectories = directories + cmd.dryRun = dryRun + cmd.ignore = ignore + return cmd.call() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CloneOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CloneOp.groovy new file mode 100644 index 0000000..85b9901 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CloneOp.groovy @@ -0,0 +1,82 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Credentials +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.auth.TransportOpUtil +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.CoercionUtil +import org.eclipse.jgit.api.CloneCommand + +/** + * Clones an existing repository. Returns a {@link org.xbib.groovy.git.Git} pointing + * to the resulting repository. + */ +@Operation('clone') +class CloneOp implements Callable { + /** + * The directory to put the cloned repository. + * @see {@link CoercionUtil#toFile(Object)} + */ + Object dir + + /** + * The URI to the repository to be cloned. + */ + String uri + + /** + * The name of the remote for the upstream repository. Defaults + * to {@code origin}. + */ + String remote = 'origin' + + /** + * {@code true} if the resulting repository should be bare, + * {@code false} (the default) otherwise. + */ + boolean bare = false + + /** + * {@code true} (the default) if a working tree should be checked out, + * {@code false} otherwise + */ + boolean checkout = true + + /** + * The remote ref that should be checked out after the repository is + * cloned. Defaults to {@code master}. + */ + String refToCheckout + + /** + * The username and credentials to use when checking out the + * repository and for subsequent remote operations on the + * repository. This is only needed if hardcoded credentials + * should be used. + */ + Credentials credentials + + Git call() { + if (!checkout && refToCheckout) { + throw new IllegalArgumentException('cannot specify a refToCheckout and set checkout to false') + } + CloneCommand cmd = org.eclipse.jgit.api.Git.cloneRepository() + TransportOpUtil.configure(cmd, credentials) + cmd.directory = CoercionUtil.toFile(dir) + cmd.setURI(uri) + cmd.remote = remote + cmd.bare = bare + cmd.noCheckout = !checkout + if (refToCheckout) { + cmd.branch = refToCheckout + } + org.eclipse.jgit.api.Git jgit = cmd.call() + Repository repo = new Repository(CoercionUtil.toFile(dir), jgit, credentials) + return new Git(repo) + } +} + diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CommitOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CommitOp.groovy new file mode 100644 index 0000000..2352918 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/CommitOp.groovy @@ -0,0 +1,85 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Person +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.CommitCommand +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.revwalk.RevCommit + +/** + * Commits staged changes to the repository. Returns the new {@code Commit}. + */ +@Operation('commit') +class CommitOp implements Callable { + private final Repository repo + + /** + * Commit message. + */ + String message + + /** + * Comment to put in the reflog. + */ + String reflogComment + + /** + * The person who committed the changes. Uses the git-config + * setting, if {@code null}. + */ + Person committer + + /** + * The person who authored the changes. Uses the git-config + * setting, if {@code null}. + */ + Person author + + /** + * Only include these paths when committing. {@code null} to + * include all staged changes. + */ + Set paths = [] + + /** + * Commit changes to all previously tracked files, even if + * they aren't staged, if {@code true}. + */ + boolean all = false + + /** + * {@code true} if the previous commit should be amended with + * these changes. + */ + boolean amend = false + + CommitOp(Repository repo) { + this.repo = repo + } + + Commit call() { + CommitCommand cmd = repo.jgit.commit() + cmd.message = message + cmd.reflogComment = reflogComment + if (committer) { + cmd.committer = new PersonIdent(committer.name, committer.email) + } + if (author) { + cmd.author = new PersonIdent(author.name, author.email) + } + paths.each { + cmd.setOnly(it) + } + if (all) { + cmd.all = all + } + cmd.amend = amend + RevCommit commit = cmd.call() + return GitUtil.convertCommit(repo, commit) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/DescribeOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/DescribeOp.groovy new file mode 100644 index 0000000..d46bb01 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/DescribeOp.groovy @@ -0,0 +1,54 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.api.DescribeCommand + +/** + * Find the nearest tag reachable. Returns an {@link String}}. + */ +@Operation('describe') +class DescribeOp implements Callable { + private final Repository repo + + DescribeOp(Repository repo){ + this.repo = repo + } + + /** + * Sets the commit to be described. Defaults to HEAD. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object commit + + /** + * Whether to always use long output format or not. + */ + boolean longDescr + + /** + * Include non-annotated tags when determining nearest tag. + */ + boolean tags + + /** + * glob patterns to match tags against before they are considered + */ + List match = [] + + String call(){ + DescribeCommand cmd = repo.jgit.describe() + if (commit) { + cmd.setTarget(new ResolveService(repo).toRevisionString(commit)) + } + cmd.setLong(longDescr) + cmd.setTags(tags) + if (match) { + cmd.setMatch(match as String[]) + } + return cmd.call() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/FetchOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/FetchOp.groovy new file mode 100644 index 0000000..50f7e8f --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/FetchOp.groovy @@ -0,0 +1,75 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.auth.TransportOpUtil +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.FetchCommand +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.TagOpt + +/** + * Fetch changes from remotes. + */ +@Operation('fetch') +class FetchOp implements Callable { + private final Repository repo + + /** + * Which remote should be fetched. + */ + String remote + + /** + * List of refspecs to fetch. + */ + List refSpecs = [] + + /** + * {@code true} if branches removed by the remote should be + * removed locally. + */ + boolean prune = false + + /** + * How should tags be handled. + */ + TagMode tagMode = TagMode.AUTO + + FetchOp(Repository repo) { + this.repo = repo + } + + /** + * Provides a string conversion to the enums. + */ + void setTagMode(String mode) { + tagMode = mode.toUpperCase() + } + + Void call() { + FetchCommand cmd = repo.jgit.fetch() + TransportOpUtil.configure(cmd, repo.credentials) + if (remote) { cmd.remote = remote } + cmd.refSpecs = refSpecs.collect { + new RefSpec(it) + } + cmd.removeDeletedRefs = prune + cmd.tagOpt = tagMode.jgit + cmd.call() + return null + } + + enum TagMode { + AUTO(TagOpt.AUTO_FOLLOW), + ALL(TagOpt.FETCH_TAGS), + NONE(TagOpt.NO_TAGS) + + final TagOpt jgit + + private TagMode(TagOpt opt) { + this.jgit = opt + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/InitOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/InitOp.groovy new file mode 100644 index 0000000..ced3194 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/InitOp.groovy @@ -0,0 +1,36 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import java.util.concurrent.Callable +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.CoercionUtil +import org.eclipse.jgit.api.InitCommand + +/** + * Initializes a new repository. Returns a {@link org.xbib.groovy.git.Git} pointing + * to the resulting repository. + */ +@Operation('init') +class InitOp implements Callable { + /** + * {@code true} if the repository should not have a + * working tree, {@code false} (the default) otherwise + */ + boolean bare = false + + /** + * The directory to initialize the repository in. + * @see {@link CoercionUtil#toFile(Object)} + */ + Object dir + + Git call() { + InitCommand cmd = org.eclipse.jgit.api.Git.init() + cmd.bare = bare + cmd.directory = CoercionUtil.toFile(dir) + org.eclipse.jgit.api.Git jgit = cmd.call() + Repository repo = new Repository(CoercionUtil.toFile(dir), jgit, null) + return new Git(repo) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LogOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LogOp.groovy new file mode 100644 index 0000000..af92473 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LogOp.groovy @@ -0,0 +1,62 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.LogCommand + +/** + * Gets a log of commits in the repository. Returns a list of {@link Commit}s. + * Since a Git history is not necessarilly a line, these commits may not be in + * a strict order. + */ +@Operation('log') +class LogOp implements Callable> { + private final Repository repo + + /** + * @see {@link ResolveService#toRevisionString(Object)} + */ + List includes = [] + /** + * @see {@link ResolveService#toRevisionString(Object)} + */ + List excludes = [] + List paths = [] + int skipCommits = -1 + int maxCommits = -1 + + LogOp(Repository repo) { + this.repo = repo + } + + void range(Object since, Object until) { + excludes << since + includes << until + } + + List call() { + LogCommand cmd = repo.jgit.log() + ResolveService resolve = new ResolveService(repo) + def toObjectId = { rev -> + String revstr = resolve.toRevisionString(rev) + GitUtil.resolveRevObject(repo, revstr, true).id + } + + includes.collect(toObjectId).each { object -> + cmd.add(object) + } + excludes.collect(toObjectId).each { object -> + cmd.not(object) + } + paths.each { path -> + cmd.addPath(path as String) + } + cmd.skip = skipCommits + cmd.maxCount = maxCommits + return cmd.call().collect { GitUtil.convertCommit(repo, it) }.asImmutable() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LsRemoteOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LsRemoteOp.groovy new file mode 100644 index 0000000..a40766f --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/LsRemoteOp.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Ref +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.auth.TransportOpUtil +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.LsRemoteCommand +import org.eclipse.jgit.lib.ObjectId + +/** + * List references in a remote repository. + */ +@Operation('lsremote') +class LsRemoteOp implements Callable> { + private final Repository repo + + String remote = 'origin' + + boolean heads = false + + boolean tags = false + + LsRemoteOp(Repository repo) { + this.repo = repo + } + + Map call() { + LsRemoteCommand cmd = repo.jgit.lsRemote() + TransportOpUtil.configure(cmd, repo.credentials) + cmd.remote = remote + cmd.heads = heads + cmd.tags = tags + return cmd.call().collectEntries { jgitRef -> + Ref ref = new Ref(jgitRef.getName()) + [(ref): ObjectId.toString(jgitRef.getObjectId())] + }.asImmutable() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/MergeOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/MergeOp.groovy new file mode 100644 index 0000000..3964d42 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/MergeOp.groovy @@ -0,0 +1,124 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.MergeCommand +import org.eclipse.jgit.api.MergeResult + +/** + * Merges changes from a single head. This is a simplified version of + * merge. If any conflict occurs the merge will throw an exception. The + * conflicting files can be identified with {@code grgit.status()}. + * + *

Merge another head into the current branch.

+ * + *
+ * grgit.merge(head: 'some-branch')
+ * 
+ * + *

Merge with another mode.

+ * + *
+ * grgit.merge(mode: MergeOp.Mode.ONLY_FF)
+ * 
+ */ +@Operation('merge') +class MergeOp implements Callable { + private final Repository repo + + /** + * The head to merge into the current HEAD. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object head + + /** + * The message to use for the merge commit + */ + String message + + /** + * How to handle the merge. + */ + Mode mode + + MergeOp(Repository repo) { + this.repo = repo + } + + void setMode(String mode) { + this.mode = mode.toUpperCase().replace('-', '_') + } + + Void call() { + MergeCommand cmd = repo.jgit.merge() + if (head) { + /* + * we want to preserve ref name in merge commit msg. if it's a ref, don't + * resolve down to commit id + */ + def ref = repo.jgit.repository.findRef(head) + if (ref == null) { + def revstr = new ResolveService(repo).toRevisionString(head) + cmd.include(GitUtil.resolveObject(repo, revstr)) + } else { + cmd.include(ref) + } + } + if (message) { + cmd.setMessage(message) + } + switch (mode) { + case Mode.ONLY_FF: + cmd.fastForward = MergeCommand.FastForwardMode.FF_ONLY + break + case Mode.CREATE_COMMIT: + cmd.fastForward = MergeCommand.FastForwardMode.NO_FF + break + case Mode.SQUASH: + cmd.squash = true + break + case Mode.NO_COMMIT: + cmd.commit = false + break + } + + MergeResult result = cmd.call() + if (!result.mergeStatus.successful) { + throw new IllegalStateException("could not merge (conflicting files can be retrieved with a call to grgit.status()): ${result}") + } + return null + } + + static enum Mode { + /** + * Fast-forwards if possible, creates a merge commit otherwise. + * Behaves like --ff. + */ + DEFAULT, + + /** + * Only merges if a fast-forward is possible. + * Behaves like --ff-only. + */ + ONLY_FF, + + /** + * Always creates a merge commit (even if a fast-forward is possible). + * Behaves like --no-ff. + */ + CREATE_COMMIT, + /** + * Squashes the merged changes into one set and leaves them uncommitted. + * Behaves like --squash. + */ + SQUASH, + /** + * Merges changes, but does not commit them. Behaves like --no-commit. + */ + NO_COMMIT + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/OpenOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/OpenOp.groovy new file mode 100644 index 0000000..645f8bf --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/OpenOp.groovy @@ -0,0 +1,61 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import java.util.concurrent.Callable +import org.xbib.groovy.git.Credentials +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.CoercionUtil +import org.eclipse.jgit.storage.file.FileRepositoryBuilder + +/** + * Opens an existing repository. Returns a {@link org.xbib.groovy.git.Git} pointing + * to the resulting repository. + */ +@Operation('open') +class OpenOp implements Callable { + /** + * Hardcoded credentials to use for remote operations. + */ + Credentials credentials + + /** + * The directory to open the repository from. Incompatible + * with {@code currentDir}. + * @see {@link CoercionUtil#toFile(Object)} + */ + Object dir + + /** + * The directory to begin searching from the repository + * from. Incompatible with {@code dir}. + * @see {@link CoercionUtil#toFile(Object)} + */ + Object currentDir + + Git call() { + if (dir && currentDir) { + throw new IllegalArgumentException('Cannot use both dir and currentDir.') + } else if (dir) { + def dirFile = CoercionUtil.toFile(dir) + def repo = new Repository(dirFile, org.eclipse.jgit.api.Git.open(dirFile), credentials) + return new Git(repo) + } else { + FileRepositoryBuilder builder = new FileRepositoryBuilder() + builder.readEnvironment() + if (currentDir) { + File currentDirFile = CoercionUtil.toFile(currentDir) + builder.findGitDir(currentDirFile) + } else { + builder.findGitDir() + } + if(builder.getGitDir() == null){ + throw new IllegalStateException('No .git directory found!'); + } + def jgitRepo = builder.build() + org.eclipse.jgit.api.Git jgit = new org.eclipse.jgit.api.Git(jgitRepo) + Repository repo = new Repository(jgitRepo.directory, jgit, credentials) + return new Git(repo) + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PullOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PullOp.groovy new file mode 100644 index 0000000..9e6bf8d --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PullOp.groovy @@ -0,0 +1,55 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.auth.TransportOpUtil +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.PullCommand +import org.eclipse.jgit.api.PullResult + +/** + * Pulls changes from the remote on the current branch. If the changes + * conflict, the pull will fail, any conflicts can be retrieved with + * {@code git.status()}, and throwing an exception. + */ +@Operation('pull') +class PullOp implements Callable { + private final Repository repo + + /** + * The name of the remote to pull. If not set, the current branch's + * configuration will be used. + */ + String remote + + /** + * The name of the remote branch to pull. If not set, the current branch's + * configuration will be used. + */ + String branch + + /** + * Rebase on top of the changes when they are pulled in, if + * {@code true}. {@code false} (the default) otherwise. + */ + boolean rebase = false + + PullOp(Repository repo) { + this.repo = repo + } + + Void call() { + PullCommand cmd = repo.jgit.pull() + if (remote) { cmd.remote = remote } + if (branch) { cmd.remoteBranchName = branch } + cmd.rebase = rebase + TransportOpUtil.configure(cmd, repo.credentials) + + PullResult result = cmd.call() + if (!result.successful) { + throw new IllegalStateException("Could not pull: ${result}") + } + return null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PushOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PushOp.groovy new file mode 100644 index 0000000..bf2e9d3 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/PushOp.groovy @@ -0,0 +1,91 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.PushException +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.auth.TransportOpUtil +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.PushCommand +import org.eclipse.jgit.transport.RemoteRefUpdate + +/** + * Push changes to a remote repository. + */ +@Operation('push') +class PushOp implements Callable { + private final Repository repo + + /** + * The remote to push to. + */ + String remote + + /** + * The refs or refspecs to use when pushing. If {@code null} + * and {@code all} is {@code false} only push the current branch. + */ + List refsOrSpecs = [] + + /** + * {@code true} to push all branches, {@code false} (the default) + * to only push the current one. + */ + boolean all = false + + /** + * {@code true} to push tags, {@code false} (the default) otherwise. + */ + boolean tags = false + + /** + * {@code true} if branches should be pushed even if they aren't + * a fast-forward, {@code false} (the default) if it should fail. + */ + boolean force = false + + /** + * {@code true} if result of this operation should be just estimation + * of real operation result, no real push is performed. + * {@code false} (the default) if real push to remote repo should be performed. + * + * @since 0.4.1 + */ + boolean dryRun = false + + PushOp(Repository repo) { + this.repo = repo + } + + Void call() { + PushCommand cmd = repo.jgit.push() + TransportOpUtil.configure(cmd, repo.credentials) + if (remote) { + cmd.remote = remote + } + refsOrSpecs.each { + cmd.add(it) + } + if (all) { + cmd.setPushAll() + } + if (tags) { + cmd.setPushTags() + } + cmd.force = force + cmd.dryRun = dryRun + def failures = [] + cmd.call().each { result -> + result.remoteUpdates.findAll { update -> + !(update.status == RemoteRefUpdate.Status.OK || update.status == RemoteRefUpdate.Status.UP_TO_DATE) + }.each { update -> + String info = "${update.srcRef} to ${update.remoteName}" + String message = update.message ? " (${update.message})" : '' + failures << "${info}${message}" + } + } + if (failures) { + throw new PushException("Failed to push: ${failures.join(',')}") + } + return null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteAddOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteAddOp.groovy new file mode 100644 index 0000000..553a34b --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteAddOp.groovy @@ -0,0 +1,82 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Remote +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.RemoteConfig +import org.eclipse.jgit.transport.URIish + +/** + * Adds a remote to the repository. Returns the newly created {@link Remote}. + * If remote with given name already exists, this command will fail. + */ +@Operation('add') +class RemoteAddOp implements Callable { + + private final Repository repository + + /** + * Name of the remote. + */ + String name + + /** + * URL to fetch from. + */ + String url + + /** + * URL to push to. + */ + String pushUrl + + /** + * Specs to fetch from the remote. + */ + List fetchRefSpecs = [] + + /** + * Specs to push to the remote. + */ + List pushRefSpecs = [] + + /** + * Whether or not pushes will mirror the repository. + */ + boolean mirror + + RemoteAddOp(Repository repo) { + this.repository = repo + } + + @Override + Remote call() { + Config config = repository.jgit.repository.config + if (RemoteConfig.getAllRemoteConfigs(config).find { it.name == name }) { + throw new IllegalStateException("remote $name already exists") + } + def toUri = { + url -> new URIish(url) + } + def toRefSpec = { + spec -> new RefSpec(spec) + } + RemoteConfig remote = new RemoteConfig(config, name) + if (url) { + remote.addURI(toUri(url)) + } + if (pushUrl) { + remote.addPushURI(toUri(pushUrl)) + } + remote.fetchRefSpecs = (fetchRefSpecs ?: ["+refs/heads/*:refs/remotes/$name/*"]).collect(toRefSpec) + remote.pushRefSpecs = pushRefSpecs.collect(toRefSpec) + remote.mirror = mirror + remote.update(config) + config.save() + return GitUtil.convertRemote(remote) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteListOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteListOp.groovy new file mode 100644 index 0000000..870fe5b --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RemoteListOp.groovy @@ -0,0 +1,31 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Remote +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.transport.RemoteConfig + +/** + * Lists remotes in the repository. Returns a list of {@link Remote}. + */ +@Operation('list') +class RemoteListOp implements Callable> { + private final Repository repository + + RemoteListOp(Repository repo) { + this.repository = repo + } + + @Override + List call() { + return RemoteConfig.getAllRemoteConfigs(repository.jgit.repository.config).collect { rc -> + if (rc.getURIs().size() > 1 || rc.pushURIs.size() > 1) { + throw new IllegalArgumentException("does not currently support multiple URLs in remote: [uris: ${rc.uris}, pushURIs:${rc.pushURIs}]") + } + GitUtil.convertRemote(rc) + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ResetOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ResetOp.groovy new file mode 100644 index 0000000..1c3104e --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ResetOp.groovy @@ -0,0 +1,79 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.api.ResetCommand + +/** + * Reset changes in the repository. + */ +@Operation('reset') +class ResetOp implements Callable { + private final Repository repo + + /** + * The paths to reset. + */ + Set paths = [] + + /** + * The commit to reset back to. Defaults to HEAD. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object commit + + /** + * The mode to use when resetting. + */ + Mode mode = Mode.MIXED + + ResetOp(Repository repo) { + this.repo = repo + } + + void setMode(String mode) { + this.mode = mode.toUpperCase() + } + + Void call() { + if (!paths.empty && mode != Mode.MIXED) { + throw new IllegalStateException('Cannot set mode when resetting paths.') + } + + ResetCommand cmd = repo.jgit.reset() + paths.each { cmd.addPath(it) } + if (commit) { + cmd.ref = new ResolveService(repo).toRevisionString(commit) + } + if (paths.empty) { + cmd.mode = mode.jgit + } + + cmd.call() + return null + } + + static enum Mode { + /** + * Reset the index and working tree. + */ + HARD(ResetCommand.ResetType.HARD), + /** + * Reset the index, but not the working tree. + */ + MIXED(ResetCommand.ResetType.MIXED), + /** + * Only reset the HEAD. Leave the index and working tree as-is. + */ + SOFT(ResetCommand.ResetType.SOFT) + + private final ResetCommand.ResetType jgit + + private Mode(ResetCommand.ResetType jgit) { + this.jgit = jgit + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RevertOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RevertOp.groovy new file mode 100644 index 0000000..122b57b --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RevertOp.groovy @@ -0,0 +1,42 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.RevertCommand +import org.eclipse.jgit.revwalk.RevCommit + +/** + * Revert one or more commits. Returns the new HEAD {@link Commit}. + */ +@Operation('revert') +class RevertOp implements Callable { + private final Repository repo + + /** + * List of commits to revert. + * @see {@link ResolveService#toRevisionString(Object)} + */ + List commits = [] + + RevertOp(Repository repo) { + this.repo = repo + } + + Commit call() { + RevertCommand cmd = repo.jgit.revert() + commits.each { + String revstr = new ResolveService(repo).toRevisionString(it) + cmd.include(GitUtil.resolveObject(repo, revstr)) + } + RevCommit commit = cmd.call() + if (cmd.failingResult) { + throw new IllegalStateException("Could not merge reverted commits (conflicting files can be retrieved with a call to grgit.status()): ${cmd.failingResult}") + } + return GitUtil.convertCommit(repo, commit) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RmOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RmOp.groovy new file mode 100644 index 0000000..c93186d --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/RmOp.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.eclipse.jgit.api.RmCommand + +/** + * Remove files from the index and (optionally) delete them from the working tree. + * Note that wildcards are not supported. + */ +@Operation('remove') +class RmOp implements Callable { + private final Repository repo + + /** + * The file patterns to remove. + */ + Set patterns = [] + + /** + * {@code true} if files should only be removed from the index, + * {@code false} (the default) otherwise. + */ + boolean cached = false + + RmOp(Repository repo) { + this.repo = repo + } + + Void call() { + RmCommand cmd = repo.jgit.rm() + patterns.each { cmd.addFilepattern(it) } + cmd.cached = cached + cmd.call() + return null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ShowOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ShowOp.groovy new file mode 100644 index 0000000..554c62d --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/ShowOp.groovy @@ -0,0 +1,75 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.CommitDiff +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.diff.RenameDetector +import org.eclipse.jgit.diff.DiffEntry.ChangeType +import org.eclipse.jgit.treewalk.TreeWalk + +/** + * Show changes made in a commit. + * Returns changes made in commit in the form of {@link CommitDiff}. + */ +@Operation('show') +class ShowOp implements Callable { + private final Repository repo + + /** + * The commit to show + * @see {@link org.xbib.groovy.git.service.ResolveService#toRevisionString(Object)} + */ + Object commit + + ShowOp(Repository repo) { + this.repo = repo + } + + CommitDiff call() { + if (!commit) { + throw new IllegalArgumentException('You must specify which commit to show') + } + def revString = new ResolveService(repo).toRevisionString(commit) + def commitId = GitUtil.resolveRevObject(repo, revString) + def parentId = GitUtil.resolveParents(repo, commitId).find() + + def commit = GitUtil.resolveCommit(repo, commitId) + + TreeWalk walk = new TreeWalk(repo.jgit.repository) + walk.recursive = true + + if (parentId) { + walk.addTree(parentId.tree) + walk.addTree(commitId.tree) + List initialEntries = DiffEntry.scan(walk) + RenameDetector detector = new RenameDetector(repo.jgit.repository) + detector.addAll(initialEntries) + List entries = detector.compute() + Map entriesByType = entries.groupBy { it.changeType } + + return new CommitDiff( + commit: commit, + added: entriesByType[ChangeType.ADD].collect { it.newPath }, + copied: entriesByType[ChangeType.COPY].collect { it.newPath }, + modified: entriesByType[ChangeType.MODIFY].collect { it.newPath }, + removed: entriesByType[ChangeType.DELETE].collect { it.oldPath }, + renamed: entriesByType[ChangeType.RENAME].collect { it.newPath } + ) + } else { + walk.addTree(commitId.tree) + def added = [] + while (walk.next()) { + added << walk.pathString + } + return new CommitDiff( + commit: commit, + added: added + ) + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/StatusOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/StatusOp.groovy new file mode 100644 index 0000000..fcb3ae6 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/StatusOp.groovy @@ -0,0 +1,26 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.StatusCommand + +/** + * Gets the current status of the repository. Returns an {@link Status}. + */ +@Operation('status') +class StatusOp implements Callable { + private final Repository repo + + StatusOp(Repository repo) { + this.repo = repo + } + + Status call() { + StatusCommand cmd = repo.jgit.status() + return GitUtil.convertStatus(cmd.call()) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagAddOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagAddOp.groovy new file mode 100644 index 0000000..0dff748 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagAddOp.groovy @@ -0,0 +1,73 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Person +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Tag +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.TagCommand +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.lib.Ref + +/** + * Adds a tag to the repository. Returns the newly created {@link Tag}. + */ +@Operation('add') +class TagAddOp implements Callable { + private final Repository repo + + /** + * The name of the tag to create. + */ + String name + + /** + * The message to put on the tag. + */ + String message + + /** + * The person who created the tag. + */ + Person tagger + + /** + * {@code true} (the default) if an annotated tag should be + * created, {@code false} otherwise. + */ + boolean annotate = true + + /** + * {@code true} to overwrite an existing tag, {@code false} + * (the default) otherwise + */ + boolean force = false + + /** + * The commit the tag should point to. + * @see {@link ResolveService#toRevisionString(Object)} + */ + Object pointsTo + + TagAddOp(Repository repo) { + this.repo = repo + } + + Tag call() { + TagCommand cmd = repo.jgit.tag() + cmd.name = name + cmd.message = message + if (tagger) { cmd.tagger = new PersonIdent(tagger.name, tagger.email) } + cmd.annotated = annotate + cmd.forceUpdate = force + if (pointsTo) { + def revstr = new ResolveService(repo).toRevisionString(pointsTo) + cmd.objectId = GitUtil.resolveRevObject(repo, revstr) + } + + Ref ref = cmd.call() + return GitUtil.resolveTag(repo, ref) + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagListOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagListOp.groovy new file mode 100644 index 0000000..0a4d30a --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagListOp.groovy @@ -0,0 +1,28 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Tag +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.ListTagCommand + +/** + * Lists tags in the repository. Returns a list of {@link Tag}. + */ +@Operation('list') +class TagListOp implements Callable> { + private final Repository repo + + TagListOp(Repository repo) { + this.repo = repo + } + + List call() { + ListTagCommand cmd = repo.jgit.tagList() + + return cmd.call().collect { + GitUtil.resolveTag(repo, it) + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagRemoveOp.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagRemoveOp.groovy new file mode 100644 index 0000000..99f086e --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/operation/TagRemoveOp.groovy @@ -0,0 +1,33 @@ +package org.xbib.groovy.git.operation + +import java.util.concurrent.Callable +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.Operation +import org.xbib.groovy.git.service.ResolveService +import org.eclipse.jgit.api.DeleteTagCommand + +/** + * Removes one or more tags from the repository. Returns a list of + * the fully qualified tag names that were removed. + */ +@Operation('remove') +class TagRemoveOp implements Callable> { + private final Repository repo + + /** + * Names of tags to remove. + * @see {@link ResolveService#toTagName(Object)} + */ + List names = [] + + TagRemoveOp(Repository repo) { + this.repo = repo + } + + List call() { + DeleteTagCommand cmd = repo.jgit.tagDelete() + cmd.tags = names.collect { new ResolveService(repo).toTagName(it) } + + return cmd.call() + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/service/BranchService.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/BranchService.groovy new file mode 100644 index 0000000..51a33e7 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/BranchService.groovy @@ -0,0 +1,50 @@ +package org.xbib.groovy.git.service + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.WithOperations +import org.xbib.groovy.git.operation.BranchAddOp +import org.xbib.groovy.git.operation.BranchChangeOp +import org.xbib.groovy.git.operation.BranchListOp +import org.xbib.groovy.git.operation.BranchRemoveOp +import org.xbib.groovy.git.operation.BranchStatusOp +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.lib.Ref + +/** + * Provides support for performing branch-related operations on + * a Git repository. + * + *

+ * Details of each operation's properties and methods are available on the + * doc page for the class. The following operations are supported directly on + * this service instance. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.operation.BranchAddOp add}
  • + *
  • {@link org.xbib.groovy.git.operation.BranchChangeOp change}
  • + *
  • {@link org.xbib.groovy.git.operation.BranchListOp list}
  • + *
  • {@link org.xbib.groovy.git.operation.BranchRemoveOp remove}
  • + *
  • {@link org.xbib.groovy.git.operation.BranchStatusOp status}
  • + *
+ * + */ +@WithOperations(instanceOperations=[BranchListOp, BranchAddOp, BranchRemoveOp, BranchChangeOp, BranchStatusOp]) +class BranchService { + + private final Repository repository + + BranchService(Repository repository) { + this.repository = repository + } + + /** + * Gets the branch associated with the current HEAD. + * @return the branch or {@code null} if the HEAD is detached + */ + Branch getCurrent() { + Ref ref = repository.jgit.repository.exactRef('HEAD')?.target + return ref ? GitUtil.resolveBranch(repository, ref) : null + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/service/RemoteService.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/RemoteService.groovy new file mode 100644 index 0000000..6f93517 --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/RemoteService.groovy @@ -0,0 +1,29 @@ +package org.xbib.groovy.git.service + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.WithOperations +import org.xbib.groovy.git.operation.RemoteAddOp +import org.xbib.groovy.git.operation.RemoteListOp + +/** + * Provides support for remote-related operations on a Git repository. + * + *

+ * Details of each operation's properties and methods are available on the + * doc page for the class. The following operations are supported directly on + * this service instance. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.operation.RemoteAddOp add}
  • + *
  • {@link org.xbib.groovy.git.operation.RemoteListOp list}
  • + *
+ */ +@WithOperations(instanceOperations=[RemoteListOp, RemoteAddOp]) +class RemoteService { + private final Repository repository + + RemoteService(Repository repository) { + this.repository = repository + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/service/ResolveService.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/ResolveService.groovy new file mode 100644 index 0000000..07b223e --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/ResolveService.groovy @@ -0,0 +1,207 @@ +package org.xbib.groovy.git.service + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Ref +import org.xbib.groovy.git.Tag +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.lib.ObjectId + +/** + * Convenience methods to resolve various objects. + */ +class ResolveService { + private final Repository repository + + ResolveService(Repository repository) { + this.repository = repository + } + + /** + * Resolves an object ID from the given object. Can handle any of the following + * types: + * + *
    + *
  • {@link Commit}
  • + *
  • {@link Tag}
  • + *
  • {@link Branch}
  • + *
  • {@link Ref}
  • + *
+ * + * @param object the object to resolve + * @return the corresponding object id + */ + String toObjectId(Object object) { + if (object == null) { + return null + } else if (object instanceof Commit) { + return object.id + } else if (object instanceof Branch || object instanceof Tag || object instanceof Ref) { + return ObjectId.toString(repository.jgit.repository.exactRef(object.fullName).objectId) + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a commit from the given object. Can handle any of the following + * types: + * + *
    + *
  • {@link Commit}
  • + *
  • {@link Tag}
  • + *
  • {@link Branch}
  • + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
+ * + *

+ * String arguments can be in the format of any + * Git revision string. + *

+ * @param object the object to resolve + * @return the corresponding commit + */ + Commit toCommit(Object object) { + if (object == null) { + return null + } else if (object instanceof Commit) { + return object + } else if (object instanceof Tag) { + return object.commit + } else if (object instanceof Branch) { + return GitUtil.resolveCommit(repository, object.fullName) + } else if (object instanceof String || object instanceof GString) { + return GitUtil.resolveCommit(repository, object) + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a branch from the given object. Can handle any of the following + * types: + *
    + *
  • {@link Branch}
  • + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
+ * @param object the object to resolve + * @return the corresponding commit + */ + Branch toBranch(Object object) { + if (object == null) { + return null + } else if (object instanceof Branch) { + return object + } else if (object instanceof String || object instanceof GString) { + return GitUtil.resolveBranch(repository, object) + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a branch name from the given object. Can handle any of the following + * types: + *
    + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
  • {@link Branch}
  • + *
+ * @param object the object to resolve + * @return the corresponding branch name + */ + String toBranchName(Object object) { + if (object == null) { + return object + } else if (object instanceof String || object instanceof GString) { + return object + } else if (object instanceof Branch) { + return object.fullName + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a tag from the given object. Can handle any of the following + * types: + *
    + *
  • {@link Tag}
  • + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
+ * @param object the object to resolve + * @return the corresponding commit + */ + Tag toTag(Object object) { + if (object == null) { + return object + } else if (object instanceof Tag) { + return object + } else if (object instanceof String || object instanceof GString) { + GitUtil.resolveTag(repository, object) + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a tag name from the given object. Can handle any of the following + * types: + *
    + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
  • {@link Tag}
  • + *
+ * @param object the object to resolve + * @return the corresponding tag name + */ + String toTagName(Object object) { + if (object == null) { + return object + } else if (object instanceof String || object instanceof GString) { + return object + } else if (object instanceof Tag) { + return object.fullName + } else { + throwIllegalArgument(object) + } + } + + /** + * Resolves a revision string that corresponds to the given object. Can + * handle any of the following types: + *
    + *
  • {@link Commit}
  • + *
  • {@link Tag}
  • + *
  • {@link Branch}
  • + *
  • {@link String}
  • + *
  • {@link GString}
  • + *
+ * @param object the object to resolve + * @return the corresponding commit + */ + String toRevisionString(Object object) { + if (object == null) { + return object + } else if (object instanceof Commit) { + return object.id + } else if (object instanceof Tag) { + return object.fullName + } else if (object instanceof Branch) { + return object.fullName + } else if (object instanceof String || object instanceof GString) { + return object + } else { + throwIllegalArgument(object) + } + } + + private void throwIllegalArgument(Object object) { + throw new IllegalArgumentException("Can't handle the following object (${object}) of class (${object.class})") + } +} + diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/service/TagService.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/TagService.groovy new file mode 100644 index 0000000..d6e6a6d --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/service/TagService.groovy @@ -0,0 +1,33 @@ +package org.xbib.groovy.git.service + +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.internal.WithOperations +import org.xbib.groovy.git.operation.TagAddOp +import org.xbib.groovy.git.operation.TagListOp +import org.xbib.groovy.git.operation.TagRemoveOp + +/** + * Provides support for performing tag-related operations on + * a Git repository. + * + *

+ * Details of each operation's properties and methods are available on the + * doc page for the class. The following operations are supported directly on + * this service instance. + *

+ * + *
    + *
  • {@link org.xbib.groovy.git.operation.TagAddOp add}
  • + *
  • {@link org.xbib.groovy.git.operation.TagListOp list}
  • + *
  • {@link org.xbib.groovy.git.operation.TagRemoveOp remove}
  • + *
+ * + */ +@WithOperations(instanceOperations=[TagListOp, TagAddOp, TagRemoveOp]) +class TagService { + private final Repository repository + + TagService(Repository repository) { + this.repository = repository + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/util/CoercionUtil.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/util/CoercionUtil.groovy new file mode 100644 index 0000000..92fcf3a --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/util/CoercionUtil.groovy @@ -0,0 +1,19 @@ +package org.xbib.groovy.git.util + +import java.nio.file.Path + +final class CoercionUtil { + + private CoercionUtil() { + } + + static File toFile(Object obj) { + if (obj instanceof File) { + return obj + } else if (obj instanceof Path) { + return obj.toFile() + } else { + return new File(obj.toString()) + } + } +} diff --git a/groovy-git/src/main/groovy/org/xbib/groovy/git/util/GitUtil.groovy b/groovy-git/src/main/groovy/org/xbib/groovy/git/util/GitUtil.groovy new file mode 100644 index 0000000..2a21b8b --- /dev/null +++ b/groovy-git/src/main/groovy/org/xbib/groovy/git/util/GitUtil.groovy @@ -0,0 +1,246 @@ +package org.xbib.groovy.git.util + +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Person +import org.xbib.groovy.git.Remote +import org.xbib.groovy.git.Repository +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.Tag +import org.eclipse.jgit.errors.IncorrectObjectTypeException +import org.eclipse.jgit.lib.BranchConfig +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.revwalk.RevObject +import org.eclipse.jgit.revwalk.RevTag +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.transport.RemoteConfig + +/** + * Utility class to perform operations against JGit objects. + */ +class GitUtil { + + private GitUtil() { + } + + /** + * Resolves a JGit {@code ObjectId} using the given revision string. + * @param repo the Grgit repository to resolve the object from + * @param revstr the revision string to use + * @return the resolved object + */ + static ObjectId resolveObject(Repository repo, String revstr) { + ObjectId object = repo.jgit.repository.resolve(revstr) + return object + } + + /** + * Resolves a JGit {@code RevObject} using the given revision string. + * @param repo the Grgit repository to resolve the object from + * @param revstr the revision string to use + * @param peel whether or not to peel the resolved object + * @return the resolved object + */ + static RevObject resolveRevObject(Repository repo, String revstr, boolean peel = false) { + ObjectId id = resolveObject(repo, revstr) + RevWalk walk = new RevWalk(repo.jgit.repository) + RevObject rev = walk.parseAny(id) + return peel ? walk.peel(rev) : rev + } + + /** + * Resolves the parents of an object. + * @param repo the Grgit repository to resolve the parents from + * @param id the object to get the parents of + * @return the parents of the commit + */ + static Set resolveParents(Repository repo, ObjectId id) { + RevWalk walk = new RevWalk(repo.jgit.repository) + RevCommit rev = walk.parseCommit(id) + return rev.parents.collect { + walk.parseCommit(it) + } + } + + /** + * Resolves a Grgit {@code Commit} using the given revision string. + * @param repo the Grgit repository to resolve the commit from + * @param revstr the revision string to use + * @return the resolved commit + */ + static Commit resolveCommit(Repository repo, String revstr) { + ObjectId id = resolveObject(repo, revstr) + return resolveCommit(repo, id) + } + + /** + * Resolves a Grgit {@code Commit} using the given object. + * @param repo the Grgit repository to resolve the commit from + * @param id the object id of the commit to resolve + * @return the resolved commit + */ + static Commit resolveCommit(Repository repo, ObjectId id) { + RevWalk walk = new RevWalk(repo.jgit.repository) + return convertCommit(repo, walk.parseCommit(id)) + } + + /** + * Converts a JGit commit to a Grgit commit. + * @param rev the JGit commit to convert + * @return a corresponding Grgit commit + */ + static Commit convertCommit(Repository repo, RevCommit rev) { + Map props = [:] + props.id = ObjectId.toString(rev) + props.abbreviatedId = repo.jgit.repository.newObjectReader().abbreviate(rev).name() + PersonIdent committer = rev.committerIdent + props.committer = new Person(committer.name, committer.emailAddress) + PersonIdent author = rev.authorIdent + props.author = new Person(author.name, author.emailAddress) + + Instant instant = Instant.ofEpochSecond(rev.commitTime) + ZoneId zone = Optional.ofNullable(rev.committerIdent.timeZone) + .map { it.toZoneId() } + .orElse(ZoneOffset.UTC) + props.dateTime = ZonedDateTime.ofInstant(instant, zone) + + props.fullMessage = rev.fullMessage + props.shortMessage = rev.shortMessage + props.parentIds = rev.parents.collect { ObjectId.toString(it) } + return new Commit(props) + } + + /** + * Resolves a Grgit tag from a name. + * @param repo the Grgit repository to resolve from + * @param name the name of the tag to resolve + * @return the resolved tag + */ + static Tag resolveTag(Repository repo, String name) { + Ref ref = repo.jgit.repository.getRef(name) + return resolveTag(repo, ref) + } + + /** + * Resolves a Grgit Tag from a JGit ref. + * @param repo the Grgit repository to resolve from + * @param ref the JGit ref to resolve + * @return the resolved tag + */ + static Tag resolveTag(Repository repo, Ref ref) { + Map props = [:] + props.fullName = ref.name + try { + RevWalk walk = new RevWalk(repo.jgit.repository) + RevTag rev = walk.parseTag(ref.objectId) + RevObject target = walk.peel(rev) + walk.parseBody(rev.object) + props.commit = convertCommit(repo, target) + PersonIdent tagger = rev.taggerIdent + props.tagger = new Person(tagger.name, tagger.emailAddress) + props.fullMessage = rev.fullMessage + props.shortMessage = rev.shortMessage + + Instant instant = rev.taggerIdent.when.toInstant() + ZoneId zone = Optional.ofNullable(rev.taggerIdent.timeZone) + .map { it.toZoneId() } + .orElse(ZoneOffset.UTC) + props.dateTime = ZonedDateTime.ofInstant(instant, zone) + } catch (IncorrectObjectTypeException e) { + props.commit = resolveCommit(repo, ref.objectId) + } + return new Tag(props) + } + + /** + * Resolves a Grgit branch from a name. + * @param repo the Grgit repository to resolve from + * @param name the name of the branch to resolve + * @return the resolved branch + */ + static Branch resolveBranch(Repository repo, String name) { + Ref ref = repo.jgit.repository.findRef(name) + return resolveBranch(repo, ref) + } + + /** + * Resolves a Grgit branch from a JGit ref. + * @param repo the Grgit repository to resolve from + * @param ref the JGit ref to resolve + * @return the resolved branch or {@code null} if the {@code ref} is + * {@code null} + */ + static Branch resolveBranch(Repository repo, Ref ref) { + if (ref == null) { + return null + } + Map props = [:] + props.fullName = ref.name + String shortName = org.eclipse.jgit.lib.Repository.shortenRefName(props.fullName) + Config config = repo.jgit.repository.config + BranchConfig branchConfig = new BranchConfig(config, shortName) + if (branchConfig.trackingBranch) { + props.trackingBranch = resolveBranch(repo, branchConfig.trackingBranch) + } + return new Branch(props) + } + + /** + * Converts a JGit status to a Grgit status. + * @param jgitStatus the status to convert + * @return the converted status + */ + static Status convertStatus(org.eclipse.jgit.api.Status jgitStatus) { + return new Status( + staged: [ + added: jgitStatus.added, + modified: jgitStatus.changed, + removed: jgitStatus.removed + ], + unstaged: [ + added: jgitStatus.untracked, + modified: jgitStatus.modified, + removed: jgitStatus.missing + ], + conflicts: jgitStatus.conflicting + ) + } + + /** + * Converts a JGit remote to a Grgit remote. + * @param rc the remote config to convert + * @return the converted remote + */ + static Remote convertRemote(RemoteConfig rc) { + return new Remote( + name: rc.name, + url: rc.uris.find(), + pushUrl: rc.pushURIs.find(), + fetchRefSpecs: rc.fetchRefSpecs.collect { it.toString() }, + pushRefSpecs: rc.pushRefSpecs.collect { it.toString() }, + mirror: rc.mirror) + } + + /** + * Checks if {@code base} is an ancestor of {@code tip}. + * @param repo the repository to look in + * @param base the version that might be an ancestor + * @param tip the tip version + */ + static boolean isAncestorOf(Repository repo, Commit base, Commit tip) { + org.eclipse.jgit.lib.Repository jgit = repo.jgit.repo + RevWalk revWalk = new RevWalk(jgit) + RevCommit baseCommit = revWalk.lookupCommit(jgit.resolve(base.id)) + RevCommit tipCommit = revWalk.lookupCommit(jgit.resolve(tip.id)) + return revWalk.isMergedInto(baseCommit, tipCommit) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/Categories.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/Categories.groovy new file mode 100644 index 0000000..da24005 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/Categories.groovy @@ -0,0 +1,13 @@ +package org.xbib.groovy.git + +interface PlatformSpecific { +} + +interface WindowsSpecific extends PlatformSpecific { +} + +interface LinuxSpecific extends PlatformSpecific { +} + +interface MacSpecific extends PlatformSpecific { +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/GitTestUtil.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/GitTestUtil.groovy new file mode 100644 index 0000000..7ba632e --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/GitTestUtil.groovy @@ -0,0 +1,63 @@ +package org.xbib.groovy.git + +import org.xbib.groovy.git.util.GitUtil +import org.eclipse.jgit.api.ListBranchCommand.ListMode +import org.eclipse.jgit.transport.RemoteConfig + +final class GitTestUtil { + + private GitTestUtil() { + } + + static File repoFile(Git grgit, String path, boolean makeDirs = true) { + def file = new File(grgit.repository.rootDir, path) + if (makeDirs) file.parentFile.mkdirs() + return file + } + + static File repoDir(Git grgit, String path) { + def file = new File(grgit.repository.rootDir, path) + file.mkdirs() + return file + } + + static Branch branch(String fullName, String trackingBranchFullName = null) { + Branch trackingBranch = trackingBranchFullName ? branch(trackingBranchFullName) : null + return new Branch(fullName, trackingBranch) + } + + static List branches(Git grgit, boolean trim = false) { + return grgit.repository.jgit.branchList().with { + listMode = ListMode.ALL + delegate.call() + }.collect { trim ? it.name - 'refs/heads/' : it.name } + } + + static List remoteBranches(Git grgit) { + return grgit.repository.jgit.branchList().with { + listMode = ListMode.REMOTE + delegate.call() + }.collect { it.name - 'refs/remotes/origin/' } + } + + static List tags(Git grgit) { + return grgit.repository.jgit.tagList().call().collect { + it.name - 'refs/tags/' + } + } + + static List remotes(Git grgit) { + def jgitConfig = grgit.repository.jgit.getRepository().config + return RemoteConfig.getAllRemoteConfigs(jgitConfig).collect { it.name} + } + + static Commit resolve(Git grgit, String revstr) { + return GitUtil.resolveCommit(grgit.repository, revstr) + } + + static void configure(Git grgit, Closure closure) { + def config = grgit.repository.jgit.getRepository().config + config.with(closure) + config.save() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/MultiGitOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/MultiGitOpSpec.groovy new file mode 100644 index 0000000..88a1e4e --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/MultiGitOpSpec.groovy @@ -0,0 +1,40 @@ +package org.xbib.groovy.git + +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +import spock.lang.Specification + +class MultiGitOpSpec extends Specification { + + @Rule TemporaryFolder tempDir = new TemporaryFolder() + + Person person = new Person('Bruce Wayne', 'bruce.wayne@wayneindustries.com') + + protected Git init(String name) { + File repoDir = tempDir.newFolder(name).canonicalFile + org.eclipse.jgit.api.Git jgit = org.eclipse.jgit.api.Git.init().setDirectory(repoDir).call() + + // Don't want the user's git config to conflict with test expectations + jgit.repo.FS.userHome = null + + jgit.repo.config.with { + setString('user', null, 'name', person.name) + setString('user', null, 'email', person.email) + save() + } + Git.open(dir: repoDir) + } + + protected Git clone(String name, Git remote) { + File repoDir = tempDir.newFolder(name) + return Git.clone { + dir = repoDir + uri = remote.repository.rootDir.toURI() + } + } + + protected File repoFile(Git git, String path, boolean makeDirs = true) { + GitTestUtil.repoFile(git, path, makeDirs) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/SimpleGitOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/SimpleGitOpSpec.groovy new file mode 100644 index 0000000..c01bb41 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/SimpleGitOpSpec.groovy @@ -0,0 +1,38 @@ +package org.xbib.groovy.git + +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +import spock.lang.Specification + +class SimpleGitOpSpec extends Specification { + + @Rule TemporaryFolder tempDir = new TemporaryFolder() + + Git git + + Person person = new Person('Jörg Prante', 'joergprante@gmail.com') + + def setup() { + File repoDir = tempDir.newFolder('repo') + org.eclipse.jgit.api.Git jgit = org.eclipse.jgit.api.Git.init().setDirectory(repoDir).call() + + // Don't want the user's git config to conflict with test expectations + jgit.repo.FS.userHome = null + + jgit.repo.config.with { + setString('user', null, 'name', person.name) + setString('user', null, 'email', person.email) + save() + } + git = Git.open(dir: repoDir) + } + + protected File repoFile(String path, boolean makeDirs = true) { + return GitTestUtil.repoFile(git, path, makeDirs) + } + + protected File repoDir(String path) { + return GitTestUtil.repoDir(git, path) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/auth/AuthConfigSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/auth/AuthConfigSpec.groovy new file mode 100644 index 0000000..6330ed4 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/auth/AuthConfigSpec.groovy @@ -0,0 +1,33 @@ +package org.xbib.groovy.git.auth + +import org.xbib.groovy.git.Credentials + +import spock.lang.Specification + +class AuthConfigSpec extends Specification { + def 'getHardcodedCreds returns creds if username and password are set with properties'() { + given: + def props = [(AuthConfig.USERNAME_OPTION): 'myuser', (AuthConfig.PASSWORD_OPTION): 'mypass'] + expect: + AuthConfig.fromMap(props).getHardcodedCreds() == new Credentials('myuser', 'mypass') + } + + def 'getHardcodedCreds returns creds if username and password are set with env'() { + given: + def env = [(AuthConfig.USERNAME_ENV_VAR): 'myuser', (AuthConfig.PASSWORD_ENV_VAR): 'mypass'] + expect: + AuthConfig.fromMap([:], env).getHardcodedCreds() == new Credentials('myuser', 'mypass') + } + + def 'getHardcodedCreds returns creds if username is set and password is not'() { + given: + def props = [(AuthConfig.USERNAME_OPTION): 'myuser'] + expect: + AuthConfig.fromMap(props).getHardcodedCreds() == new Credentials('myuser', null) + } + + def 'getHardcodedCreds are not populated if username is not set'() { + expect: + !AuthConfig.fromMap([:]).getHardcodedCreds().isPopulated() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/AddOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/AddOpSpec.groovy new file mode 100644 index 0000000..3bb78b7 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/AddOpSpec.groovy @@ -0,0 +1,78 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec + +class AddOpSpec extends SimpleGitOpSpec { + + def 'adding specific file only adds that file'() { + given: + repoFile('1.txt') << '1' + repoFile('2.txt') << '2' + repoFile('test/3.txt') << '3' + when: + git.add(patterns:['1.txt']) + then: + git.status() == new Status( + staged: [added: ['1.txt']], + unstaged: [added: ['2.txt', 'test/3.txt']] + ) + } + + def 'adding specific directory adds all files within it'() { + given: + repoFile('1.txt') << '1' + repoFile('something/2.txt') << '2' + repoFile('test/3.txt') << '3' + repoFile('test/4.txt') << '4' + repoFile('test/other/5.txt') << '5' + when: + git.add(patterns:['test']) + then: + git.status() == new Status( + staged: [added: ['test/3.txt', 'test/4.txt', 'test/other/5.txt']], + unstaged: [added: ['1.txt', 'something/2.txt']] + ) + } + + def 'adding file pattern does not work due to lack of JGit support'() { + given: + repoFile('1.bat') << '1' + repoFile('something/2.txt') << '2' + repoFile('test/3.bat') << '3' + repoFile('test/4.txt') << '4' + repoFile('test/other/5.txt') << '5' + when: + git.add(patterns:['**/*.txt']) + then: + git.status() == new Status( + unstaged: [added: ['1.bat', 'test/3.bat', 'something/2.txt', 'test/4.txt', 'test/other/5.txt']] + ) + /* + * TODO: get it to work like this + * status.added == ['something/2.txt', 'test/4.txt', 'test/other/5.txt'] as Set + * status.untracked == ['1.bat', 'test/3.bat'] as Set + */ + } + + def 'adding with update true only adds/removes files already in the index'() { + given: + repoFile('1.bat') << '1' + repoFile('something/2.txt') << '2' + repoFile('test/3.bat') << '3' + git.add(patterns:['.']) + git.repository.jgit.commit().setMessage('Test').call() + repoFile('1.bat') << '1' + repoFile('something/2.txt') << '2' + assert repoFile('test/3.bat').delete() + repoFile('test/4.txt') << '4' + repoFile('test/other/5.txt') << '5' + when: + git.add(patterns:['.'], update:true) + then: + git.status() == new Status( + staged: [modified: ['1.bat', 'something/2.txt'], removed: ['test/3.bat']], + unstaged: [added: ['test/4.txt', 'test/other/5.txt']] + ) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ApplyOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ApplyOpSpec.groovy new file mode 100644 index 0000000..29796c0 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ApplyOpSpec.groovy @@ -0,0 +1,31 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class ApplyOpSpec extends SimpleGitOpSpec { + + def 'apply with no patch fails'() { + when: + git.apply() + then: + thrown(IllegalStateException) + } + + def 'apply with patch succeeds'() { + given: + repoFile('1.txt') << 'something' + repoFile('2.txt') << 'something else\n' + git.add(patterns:['.']) + git.commit(message: 'Test') + def patch = tempDir.newFile() + this.class.getResourceAsStream('/org/xbib/groovy/git/operation/sample.patch').withStream { stream -> + patch << stream + } + when: + git.apply(patch: patch) + then: + repoFile('1.txt').text == 'something' + repoFile('2.txt').text == 'something else\nis being added\n' + repoFile('3.txt').text == 'some new stuff\n' + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchAddOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchAddOpSpec.groovy new file mode 100644 index 0000000..7a12a7c --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchAddOpSpec.groovy @@ -0,0 +1,92 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +import spock.lang.Unroll + +class BranchAddOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + List commits = [] + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + commits << remoteGit.commit(message: 'do', all: true) + + repoFile(remoteGit, '1.txt') << '2' + commits << remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'my-branch') + + localGit = clone('local', remoteGit) + } + + def 'branch add with name creates branch pointing to current HEAD'() { + when: + localGit.branch.add(name: 'test-branch') + then: + localGit.branch.list() == [GitTestUtil.branch('refs/heads/master', 'refs/remotes/origin/master'), GitTestUtil.branch('refs/heads/test-branch')] + localGit.resolve.toCommit('test-branch') == localGit.head() + } + + def 'branch add with name and startPoint creates branch pointing to startPoint'() { + when: + localGit.branch.add(name: 'test-branch', startPoint: commits[0].id) + then: + localGit.branch.list() == [GitTestUtil.branch('refs/heads/master', 'refs/remotes/origin/master'), GitTestUtil.branch('refs/heads/test-branch')] + localGit.resolve.toCommit('test-branch') == commits[0] + } + + def 'branch add fails to overwrite existing branch'() { + given: + localGit.branch.add(name: 'test-branch', startPoint: commits[0].id) + when: + localGit.branch.add(name: 'test-branch') + then: + thrown(GitAPIException) + } + + def 'branch add with mode set but no start point fails'() { + when: + localGit.branch.add(name: 'my-branch', mode: mode) + then: + thrown(IllegalStateException) + where: + mode << BranchAddOp.Mode.values() + } + + @Unroll('branch add with #mode mode starting at #startPoint tracks #trackingBranch') + def 'branch add with mode and start point behaves correctly'() { + given: + localGit.branch.add(name: 'test-branch', startPoint: commits[0].id) + expect: + localGit.branch.add(name: 'local-branch', startPoint: startPoint, mode: mode) == GitTestUtil.branch('refs/heads/local-branch', trackingBranch) + localGit.resolve.toCommit('local-branch') == localGit.resolve.toCommit(startPoint) + where: + mode | startPoint | trackingBranch + null | 'origin/my-branch' | 'refs/remotes/origin/my-branch' + BranchAddOp.Mode.TRACK | 'origin/my-branch' | 'refs/remotes/origin/my-branch' + BranchAddOp.Mode.NO_TRACK | 'origin/my-branch' | null + null | 'test-branch' | null + BranchAddOp.Mode.TRACK | 'test-branch' | 'refs/heads/test-branch' + BranchAddOp.Mode.NO_TRACK | 'test-branch' | null + } + + @Unroll('branch add with no name, #mode mode, and a start point fails') + def 'branch add with no name fails'() { + when: + localGit.branch.add(startPoint: 'origin/my-branch', mode: mode) + then: + thrown(GitAPIException) + where: + mode << [null, BranchAddOp.Mode.TRACK, BranchAddOp.Mode.NO_TRACK] + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchChangeOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchChangeOpSpec.groovy new file mode 100644 index 0000000..03e73f4 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchChangeOpSpec.groovy @@ -0,0 +1,64 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import spock.lang.Unroll + +class BranchChangeOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + List commits = [] + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + commits << remoteGit.commit(message: 'do', all: true) + + repoFile(remoteGit, '1.txt') << '2' + commits << remoteGit.commit(message: 'do', all: true) + + remoteGit.checkout(branch: 'my-branch', createBranch: true) + + repoFile(remoteGit, '1.txt') << '3' + commits << remoteGit.commit(message: 'do', all: true) + + localGit = clone('local', remoteGit) + localGit.branch.add(name: 'local-branch') + + localGit.branch.add(name: 'test-branch', startPoint: commits[0].id) + } + + def 'branch change with non-existent branch fails'() { + when: + localGit.branch.change(name: 'fake-branch', startPoint: 'test-branch') + then: + thrown(IllegalStateException) + } + + def 'branch change with no start point fails'() { + when: + localGit.branch.change(name: 'local-branch') + then: + thrown(IllegalArgumentException) + } + + @Unroll('branch change with #mode mode starting at #startPoint tracks #trackingBranch') + def 'branch change with mode and start point behaves correctly'() { + expect: + localGit.branch.change(name: 'local-branch', startPoint: startPoint, mode: mode) == GitTestUtil.branch('refs/heads/local-branch', trackingBranch) + localGit.resolve.toCommit('local-branch') == localGit.resolve.toCommit(startPoint) + where: + mode | startPoint | trackingBranch + null | 'origin/my-branch' | 'refs/remotes/origin/my-branch' + BranchChangeOp.Mode.TRACK | 'origin/my-branch' | 'refs/remotes/origin/my-branch' + BranchChangeOp.Mode.NO_TRACK | 'origin/my-branch' | null + null | 'test-branch' | null + BranchChangeOp.Mode.TRACK | 'test-branch' | 'refs/heads/test-branch' + BranchChangeOp.Mode.NO_TRACK | 'test-branch' | null + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchListOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchListOpSpec.groovy new file mode 100644 index 0000000..26e541f --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchListOpSpec.groovy @@ -0,0 +1,53 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import spock.lang.Unroll + +class BranchListOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'my-branch') + + repoFile(remoteGit, '2.txt') << '2' + remoteGit.commit(message: 'another', all: true) + remoteGit.tag.add(name: 'test-tag'); + + localGit = clone('local', remoteGit) + } + + @Unroll('list branch with #arguments lists #expected') + def 'list branch without arguments only lists local'() { + given: + def expectedBranches = expected.collect { GitTestUtil.branch(*it) } + def head = localGit.head() + expect: + localGit.branch.list(arguments) == expectedBranches + where: + arguments | expected + [:] | [['refs/heads/master', 'refs/remotes/origin/master']] + [mode: BranchListOp.Mode.LOCAL] | [['refs/heads/master', 'refs/remotes/origin/master']] + [mode: BranchListOp.Mode.REMOTE] | [['refs/remotes/origin/master'], ['refs/remotes/origin/my-branch']] + [mode: BranchListOp.Mode.ALL] | [['refs/heads/master', 'refs/remotes/origin/master'], ['refs/remotes/origin/master'], ['refs/remotes/origin/my-branch']] + [mode: BranchListOp.Mode.REMOTE, contains: 'test-tag'] | [['refs/remotes/origin/master']] + } + + def 'list branch receives Commit object as contains flag'() { + given: + def expectedBranches = [GitTestUtil.branch('refs/remotes/origin/master')] + def head = localGit.head() + def arguments = [mode: BranchListOp.Mode.REMOTE, contains: head] + expect: + localGit.branch.list(arguments) == expectedBranches + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchRemoveOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchRemoveOpSpec.groovy new file mode 100644 index 0000000..54c0407 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchRemoveOpSpec.groovy @@ -0,0 +1,80 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Branch +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.SimpleGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +import spock.lang.Unroll + +class BranchRemoveOpSpec extends SimpleGitOpSpec { + + def setup() { + repoFile('1.txt') << '1' + git.commit(message: 'do', all: true) + + git.branch.add(name: 'branch1') + + repoFile('1.txt') << '2' + git.commit(message: 'do', all: true) + + git.branch.add(name: 'branch2') + + git.checkout(branch: 'branch3', createBranch: true) + repoFile('1.txt') << '3' + git.commit(message: 'do', all: true) + + git.checkout(branch: 'master') + } + + def 'branch remove with empty list does nothing'() { + expect: + git.branch.remove() == [] + git.branch.list() == branches('branch1', 'branch2', 'branch3', 'master') + } + + def 'branch remove with one branch removes branch'() { + expect: + git.branch.remove(names: ['branch2']) == ['refs/heads/branch2'] + git.branch.list() == branches('branch1', 'branch3', 'master') + } + + def 'branch remove with multiple branches remvoes branches'() { + expect: + git.branch.remove(names: ['branch2', 'branch1']) == ['refs/heads/branch2', 'refs/heads/branch1'] + git.branch.list() == branches('branch3', 'master') + } + + def 'branch remove with invalid branches skips invalid and removes others'() { + expect: + git.branch.remove(names: ['branch2', 'blah4']) == ['refs/heads/branch2'] + git.branch.list() == branches('branch1', 'branch3', 'master') + } + + def 'branch remove with unmerged branch and force false fails'() { + when: + git.branch.remove(names: ['branch3']) + then: + thrown(GitAPIException) + } + + def 'branch remove with unmerged branch and force true works'() { + expect: + git.branch.remove(names: ['branch3'], force: true) == ['refs/heads/branch3'] + git.branch.list() == branches('branch1', 'branch2', 'master') + } + + @Unroll('branch remove with current branch and force #force fails') + def 'branch remove with current branch fails, even with force'() { + when: + git.branch.remove(names: ['master'], force: force) + then: + thrown(GitAPIException) + where: + force << [true, false] + } + + private List branches(String... branches) { + return branches.collect { GitTestUtil.branch("refs/heads/${it}") } + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchStatusOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchStatusOpSpec.groovy new file mode 100644 index 0000000..7b73d1b --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/BranchStatusOpSpec.groovy @@ -0,0 +1,64 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.BranchStatus +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import spock.lang.Unroll + +class BranchStatusOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + remoteGit.checkout(branch: 'up-to-date', createBranch: true) + + repoFile(remoteGit, '1.txt') << '2' + remoteGit.commit(message: 'do', all: true) + + remoteGit.checkout(branch: 'master') + remoteGit.checkout(branch: 'out-of-date', createBranch: true) + + localGit = clone('local', remoteGit) + + localGit.branch.add(name: 'up-to-date', startPoint: 'origin/up-to-date') + localGit.branch.add(name: 'out-of-date', startPoint: 'origin/out-of-date') + localGit.checkout(branch: 'out-of-date') + + repoFile(remoteGit, '1.txt') << '3' + remoteGit.commit(message: 'do', all: true) + + repoFile(localGit, '1.txt') << '4' + localGit.commit(message: 'do', all: true) + repoFile(localGit, '1.txt') << '5' + localGit.commit(message: 'do', all: true) + + localGit.branch.add(name: 'no-track') + + localGit.fetch() + } + + def 'branch status on branch that is not tracking fails'() { + when: + localGit.branch.status(name: 'no-track') + then: + thrown(IllegalStateException) + } + + @Unroll('branch status on #branch gives correct counts') + def 'branch status on branch that is tracking gives correct counts'() { + expect: + localGit.branch.status(name: branch) == status + where: + branch | status + 'up-to-date' | new BranchStatus(branch: GitTestUtil.branch('refs/heads/up-to-date', 'refs/remotes/origin/up-to-date'), aheadCount: 0, behindCount: 0) + 'out-of-date' | new BranchStatus(branch: GitTestUtil.branch('refs/heads/out-of-date', 'refs/remotes/origin/out-of-date'), aheadCount: 2, behindCount: 1) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CheckoutOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CheckoutOpSpec.groovy new file mode 100644 index 0000000..87b4480 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CheckoutOpSpec.groovy @@ -0,0 +1,107 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +class CheckoutOpSpec extends SimpleGitOpSpec { + + def setup() { + repoFile('1.txt') << '1' + git.add(patterns: ['1.txt']) + git.commit(message: 'do') + + repoFile('1.txt') << '2' + git.add(patterns: ['1.txt']) + git.commit(message: 'do') + + git.branch.add(name: 'my-branch') + + repoFile('1.txt') << '3' + git.add(patterns: ['1.txt']) + git.commit(message: 'do') + } + + def 'checkout with existing branch and createBranch false works'() { + when: + git.checkout(branch: 'my-branch') + then: + git.head() == git.resolve.toCommit('my-branch') + git.branch.getCurrent().fullName == 'refs/heads/my-branch' + git.log().size() == 2 + repoFile('1.txt').text == '12' + } + + def 'checkout with existing branch, createBranch true fails'() { + when: + git.checkout(branch: 'my-branch', createBranch: true) + then: + thrown(GitAPIException) + } + + def 'checkout with non-existent branch and createBranch false fails'() { + when: + git.checkout(branch: 'fake') + then: + thrown(GitAPIException) + } + + def 'checkout with non-existent branch and createBranch true works'() { + when: + git.checkout(branch: 'new-branch', createBranch: true) + then: + git.branch.getCurrent().fullName == 'refs/heads/new-branch' + git.head() == git.resolve.toCommit('master') + git.log().size() == 3 + repoFile('1.txt').text == '123' + } + + def 'checkout with non-existent branch, createBranch true, and startPoint works'() { + when: + git.checkout(branch: 'new-branch', createBranch: true, startPoint: 'my-branch') + then: + git.branch.getCurrent().fullName == 'refs/heads/new-branch' + git.head() == git.resolve.toCommit('my-branch') + git.log().size() == 2 + repoFile('1.txt').text == '12' + } + + def 'checkout with no branch name and createBranch true fails'() { + when: + git.checkout(createBranch: true) + then: + thrown(IllegalArgumentException) + } + + def 'checkout with existing branch and orphan true fails'() { + when: + git.checkout(branch: 'my-branch', orphan: true) + then: + thrown(GitAPIException) + } + + def 'checkout with non-existent branch and orphan true works'() { + when: + git.checkout(branch: 'orphan-branch', orphan: true) + then: + git.branch.getCurrent().fullName == 'refs/heads/orphan-branch' + git.status() == new Status(staged: [added: ['1.txt']]) + repoFile('1.txt').text == '123' + } + + def 'checkout with non-existent branch, orphan true, and startPoint works'() { + when: + git.checkout(branch: 'orphan-branch', orphan: true, startPoint: 'my-branch') + then: + git.branch.getCurrent().fullName == 'refs/heads/orphan-branch' + git.status() == new Status(staged: [added: ['1.txt']]) + repoFile('1.txt').text == '12' + } + + def 'checkout with no branch name and orphan true fails'() { + when: + git.checkout(orphan: true) + then: + thrown(IllegalArgumentException) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CleanOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CleanOpSpec.groovy new file mode 100644 index 0000000..e4bf145 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CleanOpSpec.groovy @@ -0,0 +1,65 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class CleanOpSpec extends SimpleGitOpSpec { + def setup() { + repoFile('.gitignore') << 'build/\n.project' + repoFile('1.txt') << '.' + repoFile('2.txt') << '.' + repoFile('3.txt') << '.' + repoFile('dir1/4.txt') << '.' + repoFile('dir1/5.txt') << '.' + repoFile('dir1/6.txt') << '.' + repoFile('dir2/7.txt') << '.' + repoFile('dir2/8.txt') << '.' + repoDir('dir1/dir3') + repoDir('dir2/dir4') + repoDir('dir5') + repoFile('build/8.txt') << '.' + repoFile('.project') << '.' + + git.add(patterns: ['.gitignore', '1.txt', '2.txt', 'dir1', 'dir2/8.txt']) + git.commit(message: 'do') + } + + def 'clean with defaults deletes untracked files only'() { + given: + def expected = ['3.txt', 'dir2/7.txt'] as Set + expect: + git.clean() == expected + expected.every { !repoFile(it).exists() } + } + + def 'clean with paths only deletes from paths'() { + given: + def expected = ['dir2/7.txt'] as Set + expect: + git.clean(paths: ['dir2/7.txt']) == expected + expected.every { !repoFile(it).exists() } + } + + def 'clean with directories true also deletes untracked directories'() { + given: + def expected = ['3.txt', 'dir2/7.txt', 'dir5/', 'dir2/dir4/', 'dir1/dir3/'] as Set + expect: + git.clean(directories: true) == expected + expected.every { !repoFile(it).exists() } + } + + def 'clean with ignore false also deletes files ignored by .gitignore'() { + given: + def expected = ['3.txt', 'dir2/7.txt', '.project'] as Set + expect: + git.clean(ignore: false) == expected + expected.every { !repoFile(it).exists() } + } + + def 'clean with dry run true returns expected but does not delete them'() { + given: + def expected = ['3.txt', 'dir2/7.txt'] as Set + expect: + git.clean(dryRun: true) == expected + expected.every { repoFile(it).exists() } + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CloneOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CloneOpSpec.groovy new file mode 100644 index 0000000..68dca0b --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CloneOpSpec.groovy @@ -0,0 +1,129 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +class CloneOpSpec extends MultiGitOpSpec { + File repoDir + + Git remoteGit + + String remoteUri + + def remoteBranchesFilter = { it =~ $/^refs/remotes/origin/$ } + def localBranchesFilter = { it =~ $/^refs/heads/$ } + def lastName = { it.split('/')[-1] } + + def setup() { + // TODO: Convert branching and tagging to Grgit. + repoDir = tempDir.newFolder('local') + + remoteGit = init('remote') + remoteUri = remoteGit.repository.rootDir.toURI() + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'branch1') + + repoFile(remoteGit, '1.txt') << '2' + remoteGit.commit(message: 'do', all: true) + + remoteGit.tag.add(name: 'tag1') + + repoFile(remoteGit, '1.txt') << '3' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'branch2') + } + + def 'clone with non-existent uri fails'() { + when: + Git.clone(dir: repoDir, uri: 'file:///bad/uri') + then: + thrown(GitAPIException) + } + + def 'clone with default settings clones as expected'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri) + then: + git.head() == remoteGit.head() + GitTestUtil.branches(git).findAll(remoteBranchesFilter).collect(lastName) == GitTestUtil.branches(remoteGit).collect(lastName) + GitTestUtil.branches(git).findAll(localBranchesFilter).collect(lastName) == ['master'] + GitTestUtil.tags(git).collect(lastName) == ['tag1'] + GitTestUtil.remotes(git) == ['origin'] + } + + def 'clone with different remote does not use origin'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, remote: 'oranges') + then: + GitTestUtil.remotes(git) == ['oranges'] + } + + def 'clone with bare true does not have a working tree'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, bare: true) + then: + !repoFile(git, '.', false).listFiles().collect { it.name }.contains('.git') + } + + def 'clone with checkout false does not check out a working tree'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, checkout: false) + then: + repoFile(git, '.', false).listFiles().collect { it.name } == ['.git'] + } + + def 'clone with checkout false and refToCheckout set fails'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, checkout: false, refToCheckout: 'branch2') + then: + thrown(IllegalArgumentException) + } + + def 'clone with refToCheckout set to simple branch name works'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, refToCheckout: 'branch1') + then: + git.head() == remoteGit.resolve.toCommit('branch1') + GitTestUtil.branches(git).findAll(remoteBranchesFilter).collect(lastName) == GitTestUtil.branches(remoteGit).collect(lastName) + GitTestUtil.branches(git).findAll(localBranchesFilter).collect(lastName) == ['branch1'] + GitTestUtil.tags(git).collect(lastName) == ['tag1'] + GitTestUtil.remotes(git) == ['origin'] + } + + def 'clone with refToCheckout set to simple tag name works'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, refToCheckout: 'tag1') + then: + git.head() == remoteGit.resolve.toCommit('tag1') + GitTestUtil.branches(git).findAll(remoteBranchesFilter).collect(lastName) == GitTestUtil.branches(remoteGit).collect(lastName) + GitTestUtil.branches(git).findAll(localBranchesFilter).collect(lastName) == [] + GitTestUtil.tags(git).collect(lastName) == ['tag1'] + GitTestUtil.remotes(git) == ['origin'] + } + + def 'clone with refToCheckout set to full ref name works'() { + when: + def git = Git.clone(dir: repoDir, uri: remoteUri, refToCheckout: 'refs/heads/branch2') + then: + git.head() == remoteGit.resolve.toCommit('branch2') + GitTestUtil.branches(git).findAll(remoteBranchesFilter).collect(lastName) == GitTestUtil.branches(remoteGit).collect(lastName) + GitTestUtil.branches(git).findAll(localBranchesFilter).collect(lastName) == ['branch2'] + GitTestUtil.tags(git).collect(lastName) == ['tag1'] + GitTestUtil.remotes(git) == ['origin'] + } + + def 'cloned repo can be deleted'() { + given: + def git = Git.clone(dir: repoDir, uri: remoteUri, refToCheckout: 'refs/heads/branch2') + when: + git.close() + then: + repoDir.deleteDir() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CommitOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CommitOpSpec.groovy new file mode 100644 index 0000000..0e7bb81 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/CommitOpSpec.groovy @@ -0,0 +1,103 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Person +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.SimpleGitOpSpec + +class CommitOpSpec extends SimpleGitOpSpec { + + def setup() { + GitTestUtil.configure(git) { + setString('user', null, 'name', 'Alfred Pennyworth') + setString('user', null, 'email', 'alfred.pennyworth@wayneindustries.com') + } + + repoFile('1.txt') << '1' + repoFile('2.txt') << '1' + repoFile('folderA/1.txt') << '1' + repoFile('folderA/2.txt') << '1' + repoFile('folderB/1.txt') << '1' + repoFile('folderC/1.txt') << '1' + git.add(patterns:['.']) + git.commit(message: 'Test') + repoFile('1.txt') << '2' + repoFile('folderA/1.txt') << '2' + repoFile('folderA/2.txt') << '2' + repoFile('folderB/1.txt') << '2' + repoFile('folderB/2.txt') << '2' + } + + def 'commit with all false commits changes from index'() { + given: + git.add(patterns:['folderA']) + when: + git.commit(message:'Test2') + then: + git.log().size() == 2 + git.status() == new Status( + unstaged: [added: ['folderB/2.txt'], modified: ['1.txt', 'folderB/1.txt']]) + } + + def 'commit with all true commits changes in previously tracked files'() { + when: + git.commit(message:'Test2', all: true) + then: + git.log().size() == 2 + git.status() == new Status( + unstaged: [added: ['folderB/2.txt']]) + } + + def 'commit amend changes the previous commit'() { + given: + git.add(patterns:['folderA']) + when: + git.commit(message:'Test2', amend: true) + then: + git.log().size() == 1 + git.status() == new Status( + unstaged: [added: ['folderB/2.txt'], modified: ['1.txt', 'folderB/1.txt']]) + } + + def 'commit with paths only includes the specified paths from the index'() { + given: + git.add(patterns:['.']) + when: + git.commit(message:'Test2', paths:['folderA']) + then: + git.log().size() == 2 + git.status() == new Status( + staged: [added: ['folderB/2.txt'], modified: ['1.txt', 'folderB/1.txt']]) + } + + def 'commit without specific committer or author uses repo config'() { + given: + git.add(patterns:['folderA']) + when: + def commit = git.commit(message:'Test2') + then: + commit.committer == new Person('Alfred Pennyworth', 'alfred.pennyworth@wayneindustries.com') + commit.author == new Person('Alfred Pennyworth', 'alfred.pennyworth@wayneindustries.com') + git.log().size() == 2 + git.status() == new Status( + unstaged: [added: ['folderB/2.txt'], modified: ['1.txt', 'folderB/1.txt']]) + } + + def 'commit with specific committer and author uses those'() { + given: + git.add(patterns:['folderA']) + def bruce = new Person('Bruce Wayne', 'bruce.wayne@wayneindustries.com') + def lucius = new Person('Lucius Fox', 'lucius.fox@wayneindustries.com') + when: + def commit = git.commit { + message = 'Test2' + committer = lucius + author = bruce + } + then: + commit.committer == lucius + commit.author == bruce + git.log().size() == 2 + git.status() == new Status(unstaged: [added: ['folderB/2.txt'], modified: ['1.txt', 'folderB/1.txt']]) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/DescribeOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/DescribeOpSpec.groovy new file mode 100644 index 0000000..f6cced8 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/DescribeOpSpec.groovy @@ -0,0 +1,55 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class DescribeOpSpec extends SimpleGitOpSpec { + + def setup() { + git.commit(message:'initial commit') + git.tag.add(name:'initial') + git.commit(message:'another commit') + git.tag.add(name:'another') + git.commit(message:'other commit') + git.tag.add(name:'other', annotate: false) + } + + def 'with tag'() { + given: + git.reset(commit: 'HEAD~1', mode: 'hard') + expect: + git.describe() == 'another' + } + + def 'with additional commit'(){ + given: + repoFile('1.txt') << '1' + git.add(patterns:['1.txt']) + git.commit(message: 'another commit') + expect: + git.describe().startsWith('another-2-') + } + + def 'from different commit'(){ + given: + repoFile('1.txt') << '1' + git.add(patterns:['1.txt']) + git.commit(message: 'another commit') + expect: + git.describe(commit: 'HEAD~3') == 'initial' + } + + def 'with long description'() { + expect: + git.describe(longDescr: true).startsWith('another-1-') + } + + def 'with un-annotated tags'() { + expect: + git.describe(tags: true) == 'other' + } + + def 'with match'() { + expect: + git.describe(match: ['initial*']).startsWith('initial-2-') + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/FetchOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/FetchOpSpec.groovy new file mode 100644 index 0000000..af10346 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/FetchOpSpec.groovy @@ -0,0 +1,110 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +import spock.lang.Unroll + +class FetchOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + def setup() { + // TODO: convert after branch and tag available + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'my-branch') + + localGit = clone('local', remoteGit) + + repoFile(remoteGit, '1.txt') << '2' + remoteGit.commit(message: 'do', all: true) + + remoteGit.tag.add(name: 'reachable-tag') + remoteGit.branch.add(name: 'sub/mine1') + + remoteGit.checkout { + branch = 'unreachable-branch' + createBranch = true + } + + repoFile(remoteGit, '1.txt') << '2.5' + remoteGit.commit(message: 'do-unreachable', all: true) + + remoteGit.tag.add(name: 'unreachable-tag') + + remoteGit.checkout(branch: 'master') + + repoFile(remoteGit, '1.txt') << '3' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'sub/mine2') + remoteGit.branch.remove(names: ['my-branch', 'unreachable-branch'], force: true) + } + + def 'fetch from non-existent remote fails'() { + when: + localGit.fetch(remote: 'fake') + then: + thrown(GitAPIException) + } + + def 'fetch without other settings, brings down correct commits'() { + given: + def remoteHead = remoteGit.log(maxCommits: 1).find() + def localHead = { -> GitTestUtil.resolve(localGit, 'refs/remotes/origin/master') } + assert localHead() != remoteHead + when: + localGit.fetch() + then: + localHead() == remoteHead + } + + def 'fetch with prune true, removes refs deleted in the remote'() { + given: + assert GitTestUtil.remoteBranches(localGit) - GitTestUtil.branches(remoteGit, true) + when: + localGit.fetch(prune: true) + then: + GitTestUtil.remoteBranches(localGit) == GitTestUtil.branches(remoteGit, true) + } + + @Unroll('fetch with tag mode #mode fetches #expectedTags') + def 'fetch with different tag modes behave as expected'() { + given: + assert !GitTestUtil.tags(localGit) + when: + localGit.fetch(tagMode: mode) + then: + assert GitTestUtil.tags(localGit) == expectedTags + where: + mode | expectedTags + 'none' | [] + 'auto' | ['reachable-tag'] + 'all' | ['reachable-tag', 'unreachable-tag'] + } + + def 'fetch with refspecs fetches those branches'() { + given: + assert GitTestUtil.branches(localGit) == [ + 'refs/heads/master', + 'refs/remotes/origin/master', + 'refs/remotes/origin/my-branch'] + when: + localGit.fetch(refSpecs: ['+refs/heads/sub/*:refs/remotes/origin/banana/*']) + then: + GitTestUtil.branches(localGit) == [ + 'refs/heads/master', + 'refs/remotes/origin/banana/mine1', + 'refs/remotes/origin/banana/mine2', + 'refs/remotes/origin/master', + 'refs/remotes/origin/my-branch'] + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/InitOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/InitOpSpec.groovy new file mode 100644 index 0000000..1516fda --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/InitOpSpec.groovy @@ -0,0 +1,43 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.GitTestUtil + +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +import spock.lang.Specification + +class InitOpSpec extends Specification { + + @Rule TemporaryFolder tempDir = new TemporaryFolder() + + File repoDir + + def setup() { + repoDir = tempDir.newFolder('repo') + } + + def 'init with bare true does not have a working tree'() { + when: + def grgit = Git.init(dir: repoDir, bare: true) + then: + !GitTestUtil.repoFile(grgit, '.', false).listFiles().collect { it.name }.contains('.git') + } + + def 'init with bare false has a working tree'() { + when: + def grgit = Git.init(dir: repoDir, bare: false) + then: + GitTestUtil.repoFile(grgit, '.', false).listFiles().collect { it.name } == ['.git'] + } + + def 'init repo can be deleted after being closed'() { + given: + def grgit = Git.init(dir: repoDir, bare: false) + when: + grgit.close() + then: + repoDir.deleteDir() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LogOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LogOpSpec.groovy new file mode 100644 index 0000000..38f61f1 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LogOpSpec.groovy @@ -0,0 +1,77 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec +import org.xbib.groovy.git.util.GitUtil + +import org.eclipse.jgit.merge.MergeStrategy + +class LogOpSpec extends SimpleGitOpSpec { + List commits = [] + + def intToCommit = { commits[it] } + + def setup() { + File testFile1 = repoFile('1.txt') + File testFile2 = repoFile('2.txt') + + testFile1 << '1' + testFile2 << '2.1' + git.add(patterns: ['.']) + commits << git.commit(message: 'first commit\ntesting') + + testFile1 << '2' + git.add(patterns: ['.']) + commits << git.commit(message: 'second commit') + git.tag.add(name: 'v1.0.0', message: 'annotated tag') + + git.checkout(branch: intToCommit(0).id) + testFile1 << '3' + git.add(patterns: ['.']) + commits << git.commit(message: 'third commit') + + git.checkout(branch: 'master') + def jgitId = GitUtil.resolveObject(git.repository, commits[2].id) + def mergeCommit = git.repository.jgit.merge().include(jgitId).setStrategy(MergeStrategy.OURS).call().newHead + commits << GitUtil.convertCommit(git.repository, mergeCommit) + + testFile1 << '4' + git.add(patterns: ['.']) + commits << git.commit(message: 'fifth commit') + + testFile2 << '2.2' + git.add(patterns: ['.']) + commits << git.commit(message: 'sixth commit') + } + + def 'log with no arguments returns all commits'() { + expect: + git.log() == [5, 4, 3, 1, 2, 0].collect(intToCommit) + } + + def 'log with max commits returns that number of commits'() { + expect: + git.log(maxCommits:2) == [5, 4].collect(intToCommit) + } + + def 'log with skip commits does not return the first x commits'() { + expect: + git.log(skipCommits:2) == [3, 1, 2, 0].collect(intToCommit) + } + + def 'log with range returns only the commits in that range'() { + expect: + git.log { + range intToCommit(2).id, intToCommit(4).id + } == [4, 3, 1].collect(intToCommit) + } + + def 'log with path includes only commits with changes for that path'() { + expect: + git.log(paths:['2.txt']).collect { it.id } == [5, 0].collect(intToCommit).collect { it.id } + } + + def 'log with annotated tag short name works'() { + expect: + git.log(includes: ['v1.0.0']) == [1, 0].collect(intToCommit) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LsRemoteOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LsRemoteOpSpec.groovy new file mode 100644 index 0000000..f8a80b2 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/LsRemoteOpSpec.groovy @@ -0,0 +1,91 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Ref +import org.xbib.groovy.git.MultiGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +class LsRemoteOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + List branches = [] + + List tags = [] + + def setup() { + remoteGit = init('remote') + + branches << remoteGit.branch.current + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + branches << remoteGit.branch.add(name: 'my-branch') + + localGit = clone('local', remoteGit) + + repoFile(remoteGit, '1.txt') << '2' + remoteGit.commit(message: 'do', all: true) + + tags << remoteGit.tag.add(name: 'reachable-tag') + branches << remoteGit.branch.add(name: 'sub/mine1') + + remoteGit.checkout { + branch = 'unreachable-branch' + createBranch = true + } + branches << remoteGit.branch.list().find { it.name == 'unreachable-branch' } + + repoFile(remoteGit, '1.txt') << '2.5' + remoteGit.commit(message: 'do-unreachable', all: true) + + tags << remoteGit.tag.add(name: 'unreachable-tag') + + remoteGit.checkout(branch: 'master') + + repoFile(remoteGit, '1.txt') << '3' + remoteGit.commit(message: 'do', all: true) + + branches << remoteGit.branch.add(name: 'sub/mine2') + + println remoteGit.branch.list() + println remoteGit.tag.list() + } + + def 'lsremote from non-existent remote fails'() { + when: + localGit.lsremote(remote: 'fake') + then: + thrown(GitAPIException) + } + + def 'lsremote returns all refs'() { + expect: + localGit.lsremote() == format([new Ref('HEAD'), branches, tags].flatten()) + } + + def 'lsremote returns branches and tags'() { + expect: + localGit.lsremote(heads: true, tags: true) == format([branches, tags].flatten()) + } + + def 'lsremote returns only branches'() { + expect: + localGit.lsremote(heads: true) == format(branches) + } + + def 'lsremote returns only tags'() { + expect: + localGit.lsremote(tags: true) == format(tags) + } + + private Map format(things) { + return things.collectEntries { refish -> + Ref ref = new Ref(refish.fullName) + [(ref): remoteGit.resolve.toObjectId(refish)] + } + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/MergeOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/MergeOpSpec.groovy new file mode 100644 index 0000000..aeb2fa1 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/MergeOpSpec.groovy @@ -0,0 +1,198 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.MultiGitOpSpec +import spock.lang.Unroll + +class MergeOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1.1\n' + remoteGit.add(patterns: ['.']) + remoteGit.commit(message: '1.1', all: true) + repoFile(remoteGit, '2.txt') << '2.1\n' + remoteGit.add(patterns: ['.']) + remoteGit.commit(message: '2.1', all: true) + + localGit = clone('local', remoteGit) + + remoteGit.checkout(branch: 'ff', createBranch: true) + + repoFile(remoteGit, '1.txt') << '1.2\n' + remoteGit.commit(message: '1.2', all: true) + repoFile(remoteGit, '1.txt') << '1.3\n' + remoteGit.commit(message: '1.3', all: true) + + remoteGit.checkout(branch: 'clean', startPoint: 'master', createBranch: true) + + repoFile(remoteGit, '3.txt') << '3.1\n' + remoteGit.add(patterns: ['.']) + remoteGit.commit(message: '3.1', all: true) + repoFile(remoteGit, '3.txt') << '3.2\n' + remoteGit.commit(message: '3.2', all: true) + + remoteGit.checkout(branch: 'conflict', startPoint: 'master', createBranch: true) + + repoFile(remoteGit, '2.txt') << '2.2\n' + remoteGit.commit(message: '2.2', all: true) + repoFile(remoteGit, '2.txt') << '2.3\n' + remoteGit.commit(message: '2.3', all: true) + + localGit.checkout(branch: 'merge-test', createBranch: true) + + repoFile(localGit, '2.txt') << '2.a\n' + localGit.commit(message: '2.a', all: true) + repoFile(localGit, '2.txt') << '2.b\n' + localGit.commit(message: '2.b', all: true) + + localGit.fetch() + } + + @Unroll('merging #head with #mode does a fast-forward merge') + def 'fast-forward merge happens when expected'() { + given: + localGit.checkout(branch: 'master') + when: + localGit.merge(head: head, mode: mode) + then: + localGit.status().clean + localGit.head() == remoteGit.resolve.toCommit(head - 'origin/') + where: + head | mode + 'origin/ff' | MergeOp.Mode.DEFAULT + 'origin/ff' | MergeOp.Mode.ONLY_FF + 'origin/ff' | MergeOp.Mode.NO_COMMIT + } + + @Unroll('merging #head with #mode creates a merge commit') + def 'merge commits created when expected'() { + given: + def oldHead = localGit.head() + def mergeHead = remoteGit.resolve.toCommit(head - 'origin/') + when: + localGit.merge(head: head, mode: mode) + then: + localGit.status().clean + + // has a merge commit + localGit.log { + includes = ['HEAD'] + excludes = [oldHead.id, mergeHead.id] + }.size() == 1 + where: + head | mode + 'origin/ff' | MergeOp.Mode.CREATE_COMMIT + 'origin/clean' | MergeOp.Mode.DEFAULT + 'origin/clean' | MergeOp.Mode.CREATE_COMMIT + } + + @Unroll('merging #head with #mode merges but leaves them uncommitted') + def 'merge left uncommitted when expected'() { + given: + def oldHead = localGit.head() + def mergeHead = remoteGit.resolve.toCommit(head - 'origin/') + when: + localGit.merge(head: head, mode: mode) + then: + localGit.status() == status + localGit.head() == oldHead + repoFile(localGit, '.git/MERGE_HEAD').text.trim() == mergeHead.id + where: + head | mode | status + 'origin/clean' | MergeOp.Mode.NO_COMMIT | new Status(staged: [added: ['3.txt']]) + } + + @Unroll('merging #head with #mode squashes changes but leaves them uncommitted') + def 'squash merge happens when expected'() { + given: + def oldHead = localGit.head() + when: + localGit.merge(head: head, mode: mode) + then: + localGit.status() == status + localGit.head() == oldHead + !repoFile(localGit, '.git/MERGE_HEAD').exists() + where: + head | mode | status + 'origin/ff' | MergeOp.Mode.SQUASH | new Status(staged: [modified: ['1.txt']]) + 'origin/clean' | MergeOp.Mode.SQUASH | new Status(staged: [added: ['3.txt']]) + } + + @Unroll('merging #head with #mode fails with correct status') + def 'merge fails as expected'() { + given: + def oldHead = localGit.head() + when: + localGit.merge(head: head, mode: mode) + then: + localGit.head() == oldHead + localGit.status() == status + thrown(IllegalStateException) + where: + head | mode | status + 'origin/clean' | MergeOp.Mode.ONLY_FF | new Status() + 'origin/conflict' | MergeOp.Mode.DEFAULT | new Status(conflicts: ['2.txt']) + 'origin/conflict' | MergeOp.Mode.ONLY_FF | new Status() + 'origin/conflict' | MergeOp.Mode.CREATE_COMMIT | new Status(conflicts: ['2.txt']) + 'origin/conflict' | MergeOp.Mode.SQUASH | new Status(conflicts: ['2.txt']) + 'origin/conflict' | MergeOp.Mode.NO_COMMIT | new Status(conflicts: ['2.txt']) + } + + def 'merge uses message if supplied'() { + given: + def oldHead = localGit.head() + def mergeHead = remoteGit.resolve.toCommit('clean') + when: + localGit.merge(head: 'origin/clean', message: 'Custom message') + then: 'all changes are committed' + localGit.status().clean + and: 'a merge commit was created' + localGit.log { + includes = ['HEAD'] + excludes = [oldHead.id, mergeHead.id] + }.size() == 1 + and: 'the merge commits message is what was passed in' + localGit.head().shortMessage == 'Custom message' + } + + def 'merge of a branch includes this in default message'() { + given: + def oldHead = localGit.head() + def mergeHead = remoteGit.resolve.toCommit('clean') + when: + localGit.merge(head: 'origin/clean') + then: 'all changes are committed' + localGit.status().clean + and: 'a merge commit was created' + localGit.log { + includes = ['HEAD'] + excludes = [oldHead.id, mergeHead.id] + }.size() == 1 + and: 'the merge commits message mentions branch name' + localGit.head().shortMessage == 'Merge remote-tracking branch \'origin/clean\' into merge-test' + } + + def 'merge of a commit includes this in default message'() { + given: + def oldHead = localGit.head() + def mergeHead = remoteGit.resolve.toCommit('clean') + when: + localGit.merge(head: mergeHead.id) + then: 'all changes are committed' + localGit.status().clean + and: 'a merge commit was created' + localGit.log { + includes = ['HEAD'] + excludes = [oldHead.id, mergeHead.id] + }.size() == 1 + and: 'the merge commits message mentions commit hash' + localGit.head().shortMessage == "Merge commit '${mergeHead.id}' into merge-test" + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/OpenOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/OpenOpSpec.groovy new file mode 100644 index 0000000..40415db --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/OpenOpSpec.groovy @@ -0,0 +1,149 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Credentials +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec +import org.eclipse.jgit.errors.RepositoryNotFoundException +import spock.lang.Ignore +import spock.util.environment.RestoreSystemProperties + +class OpenOpSpec extends SimpleGitOpSpec { + + private static final String FILE_PATH = 'the-dir/test.txt' + + Commit commit + + File subdir + + def setup() { + repoFile(FILE_PATH) << '1.1' + git.add(patterns: ['.']) + commit = git.commit(message: 'initial commit') + subdir = repoDir('the-dir') + } + + def 'open with dir fails if there is no repo in that dir'() { + when: + Git.open(dir: 'dir/with/no/repo') + then: + thrown(RepositoryNotFoundException) + } + + def 'open with dir succeeds if repo is in that directory'() { + when: + Git opened = Git.open(dir: repoDir('.')) + then: + opened.head() == commit + } + + @Ignore + @RestoreSystemProperties + def 'open without dir fails if there is no repo in the current dir'() { + given: + File workingDir = File.createTempDir() + System.setProperty('user.dir', workingDir.absolutePath) + when: + Git.open() + then: + thrown(IllegalStateException) + } + + @Ignore + @RestoreSystemProperties + def 'open without dir succeeds if current directory is repo dir'() { + given: + File dir = repoDir('.') + System.setProperty('user.dir', dir.absolutePath) + when: + Git opened = Git.open() + repoFile(FILE_PATH) << '1.2' + opened.add(patterns: [FILE_PATH]) + then: + opened.head() == commit + opened.status() == new Status(staged: [modified: [FILE_PATH]]) + } + + @Ignore + @RestoreSystemProperties + def 'open without dir succeeds if current directory is subdir of a repo'() { + given: + System.setProperty('user.dir', subdir.absolutePath) + when: + Git opened = Git.open() + repoFile(FILE_PATH) << '1.2' + then: + opened.head() == commit + opened.status() == new Status(unstaged: [modified: [FILE_PATH]]) + } + + @Ignore + @RestoreSystemProperties + def 'open without dir succeeds if .git in current dir has gitdir'() { + given: + File workDir = tempDir.newFolder() + File gitDir = tempDir.newFolder() + + org.eclipse.jgit.api.Git.cloneRepository() + .setDirectory(workDir) + .setGitDir(gitDir) + .setURI(repoDir('.').toURI().toString()) + .call() + + new File(workDir, FILE_PATH) << '1.2' + System.setProperty('user.dir', workDir.absolutePath) + when: + Git opened = Git.open() + then: + opened.head() == commit + opened.status() == new Status(unstaged: [modified: [FILE_PATH]]) + } + + @Ignore + @RestoreSystemProperties + def 'open without dir succeeds if .git in parent dir has gitdir'() { + given: + File workDir = tempDir.newFolder() + File gitDir = tempDir.newFolder() + + org.eclipse.jgit.api.Git.cloneRepository() + .setDirectory(workDir) + .setGitDir(gitDir) + .setURI(repoDir('.').toURI().toString()) + .call() + + new File(workDir, FILE_PATH) << '1.2' + System.setProperty('user.dir', new File(workDir, 'the-dir').absolutePath) + when: + Git opened = Git.open() + then: + opened.head() == commit + opened.status() == new Status(unstaged: [modified: [FILE_PATH]]) + } + + def 'open with currentDir succeeds if current directory is subdir of a repo'() { + when: + Git opened = Git.open(currentDir: subdir) + repoFile(FILE_PATH) << '1.2' + then: + opened.head() == commit + opened.status() == new Status(unstaged: [modified: [FILE_PATH]]) + } + + def 'opened repo can be deleted after being closed'() { + given: + Git opened = Git.open(dir: repoDir('.').canonicalFile) + when: + opened.close() + then: + opened.repository.rootDir.deleteDir() + } + + def 'credentials as param name should work'() { + when: + Git opened = Git.open(dir: repoDir('.'), credentials: new Credentials()) + then: + opened.head() == commit + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PullOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PullOpSpec.groovy new file mode 100644 index 0000000..9f72273 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PullOpSpec.groovy @@ -0,0 +1,159 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.MultiGitOpSpec + +class PullOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + Git otherRemoteGit + + Commit ancestorHead + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1.1\n' + remoteGit.add(patterns: ['.']) + ancestorHead = remoteGit.commit(message: '1.1', all: true) + + remoteGit.branch.add(name: 'test-branch') + + localGit = clone('local', remoteGit) + localGit.branch.add(name: 'test-branch', startPoint: 'origin/test-branch') + + otherRemoteGit = clone('remote2', remoteGit) + repoFile(otherRemoteGit, '4.txt') << '4.1\n' + otherRemoteGit.add(patterns: ['.']) + otherRemoteGit.commit(message: '4.1', all: true) + + repoFile(remoteGit, '1.txt') << '1.2\n' + remoteGit.commit(message: '1.2', all: true) + repoFile(remoteGit, '1.txt') << '1.3\n' + remoteGit.commit(message: '1.3', all: true) + + remoteGit.checkout(branch: 'test-branch') + + repoFile(remoteGit, '2.txt') << '2.1\n' + remoteGit.add(patterns: ['.']) + remoteGit.commit(message: '2.1', all: true) + repoFile(remoteGit, '2.txt') << '2.2\n' + remoteGit.commit(message: '2.2', all: true) + } + + def 'pull to local repo with no changes fast-forwards current branch only'() { + given: + def localTestBranchHead = localGit.resolve.toCommit('test-branch') + when: + localGit.pull() + then: + localGit.head() == remoteGit.resolve.toCommit('master') + localGit.resolve.toCommit('test-branch') == localTestBranchHead + } + + def 'pull to local repo with clean changes merges branches from origin'() { + given: + repoFile(localGit, '3.txt') << '3.1\n' + localGit.add(patterns: ['.']) + localGit.commit(message: '3.1') + def localHead = localGit.head() + def remoteHead = remoteGit.resolve.toCommit('master') + when: + localGit.pull() + then: + // includes all commits from remote + (remoteGit.log(includes: ['master']) - localGit.log()).size() == 0 + /* + * Go back to one pass log command when bug is fixed: + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=439675 + */ + // localGrgit.log { + // includes = [remoteHead.id] + // excludes = ['HEAD'] + // }.size() == 0 + + // has merge commit + localGit.log { + includes = ['HEAD'] + excludes = [localHead.id, remoteHead.id] + }.size() == 1 + } + + def 'pull to local repo with conflicting changes fails'() { + given: + repoFile(localGit, '1.txt') << '1.4\n' + localGit.commit(message: '1.4', all: true) + def localHead = localGit.head() + when: + localGit.pull() + then: + localGit.status() == new Status(conflicts: ['1.txt']) + localGit.head() == localHead + thrown(IllegalStateException) + } + + def 'pull to local repo with clean changes and rebase rebases changes on top of origin'() { + given: + repoFile(localGit, '3.txt') << '3.1\n' + localGit.add(patterns: ['.']) + localGit.commit(message: '3.1') + def localHead = localGit.head() + def remoteHead = remoteGit.resolve.toCommit('master') + def localCommits = localGit.log { + includes = [localHead.id] + excludes = [ancestorHead.id] + } + when: + localGit.pull(rebase: true) + then: + // includes all commits from remote + localGit.log { + includes = [remoteHead.id] + excludes = ['HEAD'] + }.size() == 0 + + // includes none of local commits + localGit.log { + includes = [localHead.id] + excludes = ['HEAD'] + } == localCommits + + // has commit comments from local + localGit.log { + includes = ['HEAD'] + excludes = [remoteHead.id] + }.collect { + it.fullMessage + } == localCommits.collect { + it.fullMessage + } + + // has state of all changes + repoFile(localGit, '1.txt').text.normalize() == '1.1\n1.2\n1.3\n' + repoFile(localGit, '3.txt').text.normalize() == '3.1\n' + } + + def 'pull to local repo from other remote fast-forwards current branch'() { + given: + def otherRemoteUri = otherRemoteGit.repository.rootDir.toURI().toString() + localGit.remote.add(name: 'other-remote', url: otherRemoteUri) + when: + localGit.pull(remote: 'other-remote') + then: + localGit.head() == otherRemoteGit.head() + } + + def 'pull to local repo from specific remote branch merges changes'() { + given: + + when: + localGit.pull(branch: 'test-branch') + then: + (remoteGit.log(includes: ['test-branch']) - localGit.log()).size() == 0 + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PushOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PushOpSpec.groovy new file mode 100644 index 0000000..bc23e3b --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/PushOpSpec.groovy @@ -0,0 +1,123 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.PushException +import org.xbib.groovy.git.GitTestUtil +import org.xbib.groovy.git.MultiGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +class PushOpSpec extends MultiGitOpSpec { + + Git localGit + + Git remoteGit + + def setup() { + remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + remoteGit.branch.add(name: 'my-branch') + + remoteGit.checkout(branch: 'some-branch', createBranch: true) + repoFile(remoteGit, '1.txt') << '1.5.1' + remoteGit.commit(message: 'do', all: true) + remoteGit.checkout(branch: 'master') + + localGit = clone('local', remoteGit) + localGit.checkout(branch: 'my-branch', createBranch: true) + + repoFile(localGit, '1.txt') << '1.5' + localGit.commit(message: 'do', all: true) + + localGit.tag.add(name: 'tag1') + + localGit.checkout(branch: 'master') + + repoFile(localGit, '1.txt') << '2' + localGit.commit(message: 'do', all: true) + + localGit.tag.add(name: 'tag2') + } + + def 'push to non-existent remote fails'() { + when: + localGit.push(remote: 'fake') + then: + thrown(GitAPIException) + } + + def 'push without other settings pushes correct commits'() { + when: + localGit.push() + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') == GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') != GitTestUtil.resolve(remoteGit, 'refs/heads/my-branch') + !GitTestUtil.tags(remoteGit) + } + + def 'push with all true pushes all branches'() { + when: + localGit.push(all: true) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') == GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') == GitTestUtil.resolve(remoteGit, 'refs/heads/my-branch') + !GitTestUtil.tags(remoteGit) + } + + def 'push with tags true pushes all tags'() { + when: + localGit.push(tags: true) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') != GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') != GitTestUtil.resolve(remoteGit, 'refs/heads/my-branch') + GitTestUtil.tags(localGit) == GitTestUtil.tags(remoteGit) + } + + def 'push with refs only pushes those refs'() { + when: + localGit.push(refsOrSpecs: ['my-branch']) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') != GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') == GitTestUtil.resolve(remoteGit, 'refs/heads/my-branch') + !GitTestUtil.tags(remoteGit) + } + + def 'push with refSpecs only pushes those refs'() { + when: + localGit.push(refsOrSpecs: ['+refs/heads/my-branch:refs/heads/other-branch']) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') != GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') != GitTestUtil.resolve(remoteGit, 'refs/heads/my-branch') + GitTestUtil.resolve(localGit, 'refs/heads/my-branch') == GitTestUtil.resolve(remoteGit, 'refs/heads/other-branch') + !GitTestUtil.tags(remoteGit) + } + + def 'push with non-fastforward fails'() { + when: + localGit.push(refsOrSpecs: ['refs/heads/master:refs/heads/some-branch']) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') != GitTestUtil.resolve(remoteGit, 'refs/heads/some-branch') + thrown(PushException) + } + + def 'push in dryRun mode does not push commits'() { + given: + def remoteMasterHead = GitTestUtil.resolve(remoteGit, 'refs/heads/master') + when: + localGit.push(dryRun: true) + then: + GitTestUtil.resolve(localGit, 'refs/heads/master') != GitTestUtil.resolve(remoteGit, 'refs/heads/master') + GitTestUtil.resolve(remoteGit, 'refs/heads/master') == remoteMasterHead + } + + def 'push in dryRun mode does not push tags'() { + given: + def remoteMasterHead = GitTestUtil.resolve(remoteGit, 'refs/heads/master') + when: + localGit.push(dryRun: true, tags: true) + then: + !GitTestUtil.tags(remoteGit) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteAddOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteAddOpSpec.groovy new file mode 100644 index 0000000..e9b6f92 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteAddOpSpec.groovy @@ -0,0 +1,18 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Remote +import org.xbib.groovy.git.SimpleGitOpSpec + +class RemoteAddOpSpec extends SimpleGitOpSpec { + + def 'remote with given name and push/fetch urls is added'() { + given: + Remote remote = new Remote( + name: 'newRemote', + url: 'http://fetch.url/', + fetchRefSpecs: ['+refs/heads/*:refs/remotes/newRemote/*']) + expect: + remote == git.remote.add(name: 'newRemote', url: 'http://fetch.url/') + [remote] == git.remote.list() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteListOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteListOpSpec.groovy new file mode 100644 index 0000000..6769b18 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RemoteListOpSpec.groovy @@ -0,0 +1,26 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Remote +import org.xbib.groovy.git.MultiGitOpSpec + +class RemoteListOpSpec extends MultiGitOpSpec { + + def 'will list all remotes'() { + given: + Git remoteGit = init('remote') + + repoFile(remoteGit, '1.txt') << '1' + remoteGit.commit(message: 'do', all: true) + + Git localGrgit = clone('local', remoteGit) + + expect: + localGrgit.remote.list() == [ + new Remote( + name: 'origin', + url: remoteGit.repository.rootDir.canonicalFile.toPath().toUri(), + fetchRefSpecs: ['+refs/heads/*:refs/remotes/origin/*']) + ] + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ResetOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ResetOpSpec.groovy new file mode 100644 index 0000000..b84142f --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ResetOpSpec.groovy @@ -0,0 +1,74 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec + +class ResetOpSpec extends SimpleGitOpSpec { + + List commits = [] + + def setup() { + repoFile('1.bat') << '1' + repoFile('something/2.txt') << '2' + repoFile('test/3.bat') << '3' + repoFile('test/4.txt') << '4' + repoFile('test/other/5.txt') << '5' + git.add(patterns:['.']) + commits << git.commit(message: 'Test') + repoFile('1.bat') << '2' + repoFile('test/3.bat') << '4' + git.add(patterns:['.']) + commits << git.commit(message: 'Test') + repoFile('1.bat') << '3' + repoFile('something/2.txt') << '2' + git.add(patterns:['.']) + repoFile('test/other/5.txt') << '6' + repoFile('test/4.txt') << '5' + } + + def 'reset soft changes HEAD only'() { + when: + git.reset(mode:'soft', commit:commits[0].id) + then: + commits[0] == git.head() + git.status() == new Status( + staged: [modified: ['1.bat', 'test/3.bat', 'something/2.txt']], + unstaged: [modified: ['test/4.txt', 'test/other/5.txt']] + ) + } + + def 'reset mixed changes HEAD and index'() { + when: + git.reset(mode:'mixed', commit:commits[0].id) + then: + commits[0] == git.head() + git.status() == new Status( + unstaged: [modified: ['1.bat', 'test/3.bat', 'test/4.txt', 'something/2.txt', 'test/other/5.txt']]) + } + + def 'reset hard changes HEAD, index, and working tree'() { + when: + git.reset(mode:'hard', commit:commits[0].id) + then: + commits[0] == git.head() + git.status().clean + } + + def 'reset with paths changes index only'() { + when: + git.reset(paths:['something/2.txt']) + then: + commits[1] == git.head() + git.status() == new Status( + staged: [modified: ['1.bat']], + unstaged: [modified: ['test/4.txt', 'something/2.txt', 'test/other/5.txt']] + ) + } + + def 'reset with paths and mode set not supported'() { + when: + git.reset(mode:'hard', paths:['.']) + then: + thrown(IllegalStateException) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RevertOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RevertOpSpec.groovy new file mode 100644 index 0000000..8f86648 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RevertOpSpec.groovy @@ -0,0 +1,44 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class RevertOpSpec extends SimpleGitOpSpec { + + List commits = [] + + def setup() { + 5.times { + repoFile("${it}.txt") << "1" + git.add(patterns:['.']) + commits << git.commit(message:'Test', all: true) + } + } + + def 'revert with no commits does nothing'() { + when: + git.revert() + then: + git.log().size() == 5 + } + + def 'revert with commits removes associated changes'() { + when: + git.revert(commits:[1, 3].collect { commits[it].id }) + then: + git.log().size() == 7 + repoFile('.').listFiles().collect { it.name }.findAll { !it.startsWith('.') } as Set == [0, 2, 4].collect { "${it}.txt" } as Set + } + + def 'revert with conflicts raises exception'() { + given: + repoFile("1.txt") << "Edited" + git.add(patterns:['.']) + commits << git.commit(message:'Modified', all: true) + when: + git.revert(commits:[1, 3].collect { commits[it].id }) + then: + thrown(IllegalStateException) + git.log().size() == 6 + git.status().conflicts.containsAll('1.txt') + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RmOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RmOpSpec.groovy new file mode 100644 index 0000000..43802bf --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/RmOpSpec.groovy @@ -0,0 +1,61 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec + +class RmOpSpec extends SimpleGitOpSpec { + + def setup() { + repoFile('1.bat') << '1' + repoFile('something/2.txt') << '2' + repoFile('test/3.bat') << '3' + repoFile('test/4.txt') << '4' + repoFile('test/other/5.txt') << '5' + git.add(patterns:['.']) + git.commit(message: 'Test') + } + + def 'removing specific file only removes that file'() { + given: + def paths = ['1.bat'] as Set + when: + git.remove(patterns:['1.bat']) + then: + git.status() == new Status(staged: [removed: paths]) + paths.every { !repoFile(it).exists() } + } + + def 'removing specific directory removes all files within it'() { + given: + def paths = ['test/3.bat', 'test/4.txt', 'test/other/5.txt'] as Set + when: + git.remove(patterns:['test']) + then: + git.status() == new Status(staged: [removed: paths]) + paths.every { !repoFile(it).exists() } + } + + def 'removing file pattern does not work due to lack of JGit support'() { + given: + def paths = ['1.bat', 'something/2.txt', 'test/3.bat', 'test/4.txt', 'test/other/5.txt'] as Set + when: + git.remove(patterns:['**/*.txt']) + then: + git.status().clean + /* + * TODO: get it to work like this + * status.removed == ['something/2.txt', 'test/4.txt', 'test/other/5.txt'] as Set + */ + paths.every { repoFile(it).exists() } + } + + def 'removing with cached true only removes files from index'() { + given: + def paths = ['something/2.txt'] as Set + when: + git.remove(patterns:['something'], cached:true) + then: + git.status() == new Status(staged: [removed: paths], unstaged: [added: paths]) + paths.every { repoFile(it).exists() } + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ShowOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ShowOpSpec.groovy new file mode 100644 index 0000000..33dc6c1 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/ShowOpSpec.groovy @@ -0,0 +1,110 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.CommitDiff +import org.xbib.groovy.git.SimpleGitOpSpec + +class ShowOpSpec extends SimpleGitOpSpec { + + def 'can show diffs in commit that added new file'() { + File fooFile = repoFile("dir1/foo.txt") + fooFile << "foo!" + git.add(patterns: ['.']) + Commit commit = git.commit(message: "Initial commit") + + expect: + git.show(commit: commit) == new CommitDiff( + commit: commit, + added: ['dir1/foo.txt'] + ) + } + + def 'can show diffs in commit that modified existing file'() { + File fooFile = repoFile("bar.txt") + fooFile << "bar!" + git.add(patterns: ['.']) + git.commit(message: "Initial commit") + + // Change existing file + fooFile << "monkey!" + git.add(patterns: ['.']) + Commit changeCommit = git.commit(message: "Added monkey") + + expect: + git.show(commit: changeCommit) == new CommitDiff( + commit: changeCommit, + modified: ['bar.txt'] + ) + } + + def 'can show diffs in commit that deleted existing file'() { + File fooFile = repoFile("bar.txt") + fooFile << "bar!" + git.add(patterns: ['.']) + git.commit(message: "Initial commit") + + // Delete existing file + git.remove(patterns: ['bar.txt']) + Commit removeCommit = git.commit(message: "Deleted file") + + expect: + git.show(commit: removeCommit) == new CommitDiff( + commit: removeCommit, + removed: ['bar.txt'] + ) + } + + def 'can show diffs in commit with multiple changes'() { + File animalFile = repoFile("animals.txt") + animalFile << "giraffe!" + git.add(patterns: ['.']) + git.commit(message: "Initial commit") + + // Change existing file + animalFile << "zebra!" + + // Add new file + File fishFile = repoFile("salmon.txt") + fishFile<< "salmon!" + git.add(patterns: ['.']) + Commit changeCommit = git.commit(message: "Add fish and update animals with zebra") + + expect: + git.show(commit: changeCommit) == new CommitDiff( + commit: changeCommit, + modified: ['animals.txt'], + added: ['salmon.txt'] + ) + } + + def 'can show diffs in commit with rename'() { + given: + repoFile('elephant.txt') << 'I have tusks.' + git.add(patterns: ['.']) + git.commit(message: 'Adding elephant.') + + repoFile('elephant.txt').renameTo(repoFile('mammoth.txt')) + git.add(patterns: ['.']) + git.remove(patterns: ['elephant.txt']) + Commit renameCommit = git.commit(message: 'Renaming to mammoth.') + + expect: + git.show(commit: renameCommit) == new CommitDiff( + commit: renameCommit, + renamed: ['mammoth.txt'] + ) + } + + def 'can show diffs based on rev string'() { + File fooFile = repoFile("foo.txt") + fooFile << "foo!" + git.add(patterns: ['.']) + Commit commit = git.commit(message: "Initial commit") + + expect: + git.show(commit: commit.id) == new CommitDiff( + commit: commit, + added: ['foo.txt'] + ) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/StatusOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/StatusOpSpec.groovy new file mode 100644 index 0000000..26e4173 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/StatusOpSpec.groovy @@ -0,0 +1,87 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.Status +import org.xbib.groovy.git.SimpleGitOpSpec + +class StatusOpSpec extends SimpleGitOpSpec { + def setup() { + 4.times { repoFile("${it}.txt") << "1" } + git.add(patterns: ['.']) + git.commit(message: 'Test') + git.checkout(branch: 'conflict', createBranch: true) + repoFile('1.txt') << '2' + git.add(patterns: ['.']) + git.commit(message: 'conflicting change') + git.checkout(branch: 'master') + repoFile('1.txt') << '3' + git.add(patterns: ['.']) + git.commit(message: 'other change') + } + + def 'with no changes all methods return empty list'() { + expect: + git.status() == new Status() + } + + def 'new unstaged file detected'() { + given: + repoFile('5.txt') << '5' + repoFile('6.txt') << '6' + expect: + git.status() == new Status(unstaged: [added: ['5.txt', '6.txt']]) + } + + def 'unstaged modified files detected'() { + given: + repoFile('2.txt') << '2' + repoFile('3.txt') << '3' + expect: + git.status() == new Status(unstaged: [modified: ['2.txt', '3.txt']]) + } + + def 'unstaged deleted files detected'() { + given: + assert repoFile('1.txt').delete() + assert repoFile('2.txt').delete() + expect: + git.status() == new Status(unstaged: [removed: ['1.txt', '2.txt']]) + } + + def 'staged new files detected'() { + given: + repoFile('5.txt') << '5' + repoFile('6.txt') << '6' + when: + git.add(patterns: ['.']) + then: + git.status() == new Status(staged: [added: ['5.txt', '6.txt']]) + } + + def 'staged modified files detected'() { + given: + repoFile('1.txt') << '5' + repoFile('2.txt') << '6' + when: + git.add(patterns: ['.']) + then: + git.status() == new Status(staged: [modified: ['1.txt', '2.txt']]) + } + + def 'staged removed files detected'() { + given: + assert repoFile('3.txt').delete() + assert repoFile('0.txt').delete() + when: + git.add(patterns: ['.'], update: true) + then: + git.status() == new Status(staged: [removed: ['3.txt', '0.txt']]) + } + + def 'conflict files detected'() { + when: + git.merge(head: 'conflict') + then: + git.status() == new Status(conflicts: ['1.txt']) + thrown(IllegalStateException) + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagAddOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagAddOpSpec.groovy new file mode 100644 index 0000000..a631755 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagAddOpSpec.groovy @@ -0,0 +1,96 @@ +package org.xbib.groovy.git.operation + +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.ChronoField + +import org.xbib.groovy.git.Tag +import org.xbib.groovy.git.SimpleGitOpSpec +import org.eclipse.jgit.api.errors.GitAPIException + +class TagAddOpSpec extends SimpleGitOpSpec { + List commits = [] + + def setup() { + repoFile('1.txt') << '1' + commits << git.commit(message: 'do', all: true) + + repoFile('1.txt') << '2' + commits << git.commit(message: 'do', all: true) + + repoFile('1.txt') << '3' + commits << git.commit(message: 'do', all: true) + } + + def 'tag add creates annotated tag pointing to current HEAD'() { + given: + Instant instant = Instant.now().with(ChronoField.NANO_OF_SECOND, 0) + ZoneId zone = ZoneId.ofOffset('GMT', ZoneId.systemDefault().getRules().getOffset(instant)) + ZonedDateTime tagTime = ZonedDateTime.ofInstant(instant, zone) + when: + git.tag.add(name: 'test-tag') + then: + git.tag.list() == [new Tag( + commits[2], + person, + 'refs/tags/test-tag', + '', + '', + tagTime + )] + git.resolve.toCommit('test-tag') == git.head() + } + + def 'tag add with annotate false creates unannotated tag pointing to current HEAD'() { + when: + git.tag.add(name: 'test-tag', annotate: false) + then: + git.tag.list() == [new Tag( + commits[2], + null, + 'refs/tags/test-tag', + null, + null, + null + )] + git.resolve.toCommit('test-tag') == git.head() + } + + def 'tag add with name and pointsTo creates tag pointing to pointsTo'() { + given: + Instant instant = Instant.now().with(ChronoField.NANO_OF_SECOND, 0) + ZoneId zone = ZoneId.ofOffset('GMT', ZoneId.systemDefault().getRules().getOffset(instant)) + ZonedDateTime tagTime = ZonedDateTime.ofInstant(instant, zone) + when: + git.tag.add(name: 'test-tag', pointsTo: commits[0].id) + then: + git.tag.list() == [new Tag( + commits[0], + person, + 'refs/tags/test-tag', + '', + '', + tagTime + )] + git.resolve.toCommit('test-tag') == commits[0] + } + + def 'tag add without force fails to overwrite existing tag'() { + given: + git.tag.add(name: 'test-tag', pointsTo: commits[0].id) + when: + git.tag.add(name: 'test-tag') + then: + thrown(GitAPIException) + } + + def 'tag add with force overwrites existing tag'() { + given: + git.tag.add(name: 'test-tag', pointsTo: commits[0].id) + when: + git.tag.add(name: 'test-tag', force: true) + then: + git.resolve.toCommit('test-tag') == git.head() + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagListOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagListOpSpec.groovy new file mode 100644 index 0000000..e5e2521 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagListOpSpec.groovy @@ -0,0 +1,25 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class TagListOpSpec extends SimpleGitOpSpec { + List commits = [] + List tags = [] + + def setup() { + repoFile('1.txt') << '1' + commits << git.commit(message: 'do', all: true) + tags << git.tag.add(name: 'tag1', message: 'My message') + + repoFile('1.txt') << '2' + commits << git.commit(message: 'do', all: true) + tags << git.tag.add(name: 'tag2', message: 'My other\nmessage') + + tags << git.tag.add(name: 'tag3', message: 'My next message.', pointsTo: 'tag1') + } + + def 'tag list lists all tags'() { + expect: + git.tag.list() == tags + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagRemoveOpSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagRemoveOpSpec.groovy new file mode 100644 index 0000000..6838b4a --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/operation/TagRemoveOpSpec.groovy @@ -0,0 +1,40 @@ +package org.xbib.groovy.git.operation + +import org.xbib.groovy.git.SimpleGitOpSpec + +class TagRemoveOpSpec extends SimpleGitOpSpec { + + def setup() { + repoFile('1.txt') << '1' + git.commit(message: 'do', all: true) + git.tag.add(name: 'tag1') + + repoFile('1.txt') << '2' + git.commit(message: 'do', all: true) + git.tag.add(name: 'tag2', annotate: false) + } + + def 'tag remove with empty list does nothing'() { + expect: + git.tag.remove() == [] + git.tag.list().collect { it.fullName } == ['refs/tags/tag1', 'refs/tags/tag2'] + } + + def 'tag remove with one tag removes tag'() { + expect: + git.tag.remove(names: ['tag2']) == ['refs/tags/tag2'] + git.tag.list().collect { it.fullName } == ['refs/tags/tag1'] + } + + def 'tag remove with multiple tags removes tags'() { + expect: + git.tag.remove(names: ['tag2', 'tag1']) as Set == ['refs/tags/tag2', 'refs/tags/tag1'] as Set + git.tag.list() == [] + } + + def 'tag remove with invalid tags skips invalid and removes others'() { + expect: + git.tag.remove(names: ['tag2', 'blah4']) == ['refs/tags/tag2'] + git.tag.list().collect { it.fullName } == ['refs/tags/tag1'] + } +} diff --git a/groovy-git/src/test/groovy/org/xbib/groovy/git/util/GitUtilSpec.groovy b/groovy-git/src/test/groovy/org/xbib/groovy/git/util/GitUtilSpec.groovy new file mode 100644 index 0000000..db0b180 --- /dev/null +++ b/groovy-git/src/test/groovy/org/xbib/groovy/git/util/GitUtilSpec.groovy @@ -0,0 +1,195 @@ +package org.xbib.groovy.git.util + +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime + +import org.xbib.groovy.git.Commit +import org.xbib.groovy.git.Git +import org.xbib.groovy.git.Person +import org.xbib.groovy.git.Repository +import org.eclipse.jgit.errors.RevisionSyntaxException +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.merge.MergeStrategy +import org.eclipse.jgit.revwalk.RevTag +import org.eclipse.jgit.revwalk.RevWalk +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +import spock.lang.Specification + +class GitUtilSpec extends Specification { + + @Rule TemporaryFolder tempDir = new TemporaryFolder() + + Repository repo + + List commits = [] + + Ref annotatedTag + + Ref unannotatedTag + + Ref taggedAnnotatedTag + + def 'resolveObject works for branch name'() { + expect: + GitUtil.resolveObject(repo, 'master') == commits[3] + } + + def 'resolveObject works for full commit hash'() { + expect: + GitUtil.resolveObject(repo, ObjectId.toString(commits[0])) == commits[0] + } + + def 'resolveObject works for abbreviated commit hash'() { + expect: + GitUtil.resolveObject(repo, ObjectId.toString(commits[0])[0..5]) == commits[0] + } + + def 'resolveObject works for full ref name'() { + expect: + GitUtil.resolveObject(repo, 'refs/heads/master') == commits[3] + } + + def 'resolveObject works for HEAD'() { + expect: + GitUtil.resolveObject(repo, 'HEAD') == commits[3] + } + + def 'resolveObject works for parent commit'() { + expect: + GitUtil.resolveObject(repo, 'master^') == commits[1] + } + + def 'resolveObject works for current commit'() { + expect: + GitUtil.resolveObject(repo, 'master^0') == commits[3] + } + + def 'resolveObject works for n-th parent'() { + expect: + GitUtil.resolveObject(repo, 'master^2') == commits[2] + } + + def 'resolveObject works for the n-th ancestor'() { + expect: + GitUtil.resolveObject(repo, 'master~2') == commits[0] + } + + def 'resolveObject fails if revision cannot be found'() { + expect: + GitUtil.resolveObject(repo, 'unreal') == null + } + + def 'resolveObject fails if revision syntax is wrong'() { + when: + GitUtil.resolveObject(repo, 'lkj!)#(*') + then: + thrown(RevisionSyntaxException) + } + + def 'convertCommit works for valid commit'() { + given: + Person person = new Person(repo.jgit.repo.config.getString('user', null, 'name'), repo.jgit.repo.config.getString('user', null, 'email')) + Instant instant = Instant.ofEpochSecond(commits[1].commitTime) + ZoneId zone = ZoneId.ofOffset('GMT', ZoneId.systemDefault().getRules().getOffset(instant)) + ZonedDateTime commitTime = ZonedDateTime.ofInstant(instant, zone) + Commit expectedCommit = new Commit( + ObjectId.toString(commits[1]), + ObjectId.toString(commits[1])[0..6], + [ObjectId.toString(commits[0])], + person, + person, + commitTime, + 'second commit', + 'second commit' + ) + expect: + def result = GitUtil.convertCommit(repo, commits[1]) + result == expectedCommit + result.dateTime.toInstant() == commitTime.toInstant() + } + + def 'resolveTag works for annotated tag ref'() { + given: + Person person = new Person(repo.jgit.repo.config.getString('user', null, 'name'), repo.jgit.repo.config.getString('user', null, 'email')) + ZonedDateTime before = ZonedDateTime.now().minusSeconds(2) + when: + def tag = GitUtil.resolveTag(repo, annotatedTag) + and: + ZonedDateTime after = ZonedDateTime.now().plusSeconds(2) + then: + tag.commit == GitUtil.convertCommit(repo, commits[0]) + tag.tagger == person + tag.fullName == 'refs/tags/v1.0.0' + tag.fullMessage == 'first tag\ntesting' + tag.shortMessage == 'first tag testing' + tag.dateTime.isAfter(before) + tag.dateTime.isBefore(after) + } + + def 'resolveTag works for unannotated tag ref'() { + given: + Person person = new Person(repo.jgit.repo.config.getString('user', null, 'name'), repo.jgit.repo.config.getString('user', null, 'email')) + ZonedDateTime before = ZonedDateTime.now().minusSeconds(2) + when: + def tag = GitUtil.resolveTag(repo, unannotatedTag) + and: + ZonedDateTime after = ZonedDateTime.now().plusSeconds(2) + then: + tag.commit == GitUtil.convertCommit(repo, commits[0]) + tag.tagger == null + tag.fullName == 'refs/tags/v2.0.0' + tag.fullMessage == null + tag.shortMessage == null + tag.dateTime == null + } + + def 'resolveTag works for a tag pointing to a tag'() { + given: + Person person = new Person(repo.jgit.repo.config.getString('user', null, 'name'), repo.jgit.repo.config.getString('user', null, 'email')) + ZonedDateTime before = ZonedDateTime.now().minusSeconds(2) + when: + def tag = GitUtil.resolveTag(repo, taggedAnnotatedTag) + and: + ZonedDateTime after = ZonedDateTime.now().plusSeconds(2) + then: + tag.commit == GitUtil.convertCommit(repo, commits[0]) + tag.tagger == person + tag.fullName == 'refs/tags/v1.1.0' + tag.fullMessage == 'testing' + tag.shortMessage == 'testing' + tag.dateTime.isAfter(before) + tag.dateTime.isBefore(after) + } + + def setup() { + File repoDir = tempDir.newFolder('repo') + org.eclipse.jgit.api.Git jgit = org.eclipse.jgit.api.Git.init().setDirectory(repoDir).call() + jgit.repo.config.with { + setString('user', null, 'name', 'Bruce Wayne') + setString('user', null, 'email', 'bruce.wayne@wayneindustries.com') + save() + } + File testFile = new File(repoDir, '1.txt') + testFile << '1\n' + jgit.add().addFilepattern(testFile.name).call() + commits << jgit.commit().setMessage('first commit\ntesting').call() + annotatedTag = jgit.tag().setName('v1.0.0').setMessage('first tag\ntesting').call() + unannotatedTag = jgit.tag().setName('v2.0.0').setAnnotated(false).call() + testFile << '2\n' + jgit.add().addFilepattern(testFile.name).call() + commits << jgit.commit().setMessage('second commit').call() + jgit.checkout().setName(ObjectId.toString(commits[0])).call() + testFile << '3\n' + jgit.add().addFilepattern(testFile.name).call() + commits << jgit.commit().setMessage('third commit').call() + jgit.checkout().setName('master').call() + commits << jgit.merge().include(commits[2]).setStrategy(MergeStrategy.OURS).call().newHead + RevTag tagV1 = new RevWalk(jgit.repository).parseTag(annotatedTag.objectId) + taggedAnnotatedTag = jgit.tag().setName('v1.1.0').setObjectId(tagV1).setMessage('testing').call() + repo = Git.open(dir: repoDir).repository + } +} diff --git a/groovy-git/src/test/resources/org/xbib/groovy/git/operation/sample.patch b/groovy-git/src/test/resources/org/xbib/groovy/git/operation/sample.patch new file mode 100644 index 0000000..d36e5ba --- /dev/null +++ b/groovy-git/src/test/resources/org/xbib/groovy/git/operation/sample.patch @@ -0,0 +1,17 @@ +diff --git a/2.txt b/2.txt +index 3dfdc59..c2e2e28 100644 +--- a/2.txt ++++ b/2.txt +@@ -1 +1,2 @@ + something else ++is being added +diff --git a/3.txt b/3.txt +new file mode 100644 +index 0000000..df5e072 +--- /dev/null ++++ b/3.txt +@@ -0,0 +1 @@ ++some new stuff +-- +1.8.1.2 + diff --git a/settings.gradle b/settings.gradle index 68fd158..53017c0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ include 'groovy-mail' include 'groovy-ftp' include 'groovy-ftps' include 'groovy-sshd' +include 'groovy-git'