File Coverage

lib/Dist/Zilla/Plugin/Git/Checkout.pm
Criterion Covered Total %
statement 113 115 98.2
branch 45 50 90.0
condition 7 9 77.7
subroutine 18 18 100.0
pod 0 1 0.0
total 183 193 94.8


line stmt bran cond sub pod time code
1             package Dist::Zilla::Plugin::Git::Checkout;
2              
3 5     5   10045018 use 5.006;
  5         46  
4 5     5   45 use strict;
  5         18  
  5         167  
5 5     5   34 use warnings;
  5         20  
  5         330  
6              
7             our $VERSION = '0.004';
8              
9 5     5   2561 use Moose;
  5         994618  
  5         45  
10              
11             with 'Dist::Zilla::Role::BeforeRelease';
12              
13 5     5   36575 use Git::Background 0.003;
  5         43615  
  5         130  
14 5     5   1296 use Git::Version::Compare ();
  5         3753  
  5         169  
15 5     5   1161 use MooseX::Types::Moose qw(Bool Str);
  5         129183  
  5         64  
16 5     5   29276 use Path::Tiny;
  5         12427  
  5         380  
17 5     5   4186 use Term::ANSIColor qw(colored);
  5         48291  
  5         4620  
18              
19 5     5   58 use namespace::autoclean;
  5         49  
  5         48  
20              
21             has branch => (
22             is => 'ro',
23             isa => Str,
24             );
25              
26             has dir => (
27             is => 'ro',
28             isa => Str,
29             lazy => 1,
30             default => sub { path( shift->repo )->basename('.git') },
31             );
32              
33             has push_url => (
34             is => 'ro',
35             isa => Str,
36             );
37              
38             has repo => (
39             is => 'ro',
40             isa => Str,
41             required => 1,
42             );
43              
44             has revision => (
45             is => 'ro',
46             isa => Str,
47             );
48              
49             has tag => (
50             is => 'ro',
51             isa => Str,
52             );
53              
54             has _is_dirty => (
55             is => 'rw',
56             isa => Bool,
57             default => 0,
58             );
59              
60             my $BRANCH = 'branch';
61             my $REVISION = 'revision';
62             my $TAG = 'tag';
63              
64             sub before_release {
65 4     4 0 320722 my ($self) = @_;
66              
67 4 100       224 return if !$self->_is_dirty;
68              
69 1 50       59 return if $self->zilla->chrome->prompt_yn(
70             'Workspace ' . $self->dir . ' is dirty and was not updated. Release anyway?',
71             { default => 0 },
72             );
73              
74 1         1992 $self->log_fatal('Aborting release');
75              
76             # should never be reached
77 0         0 die 'Aborting release'; ## no critic (ErrorHandling::RequireCarping)
78             }
79              
80             around plugin_from_config => sub {
81             my ( $orig, $plugin_class, $name, $payload, $section ) = @_;
82              
83             my $instance = $plugin_class->$orig( $name, $payload, $section );
84              
85             $instance->_run;
86              
87             return $instance;
88             };
89              
90             sub _checkout {
91 13     13   77 my ( $self, $dir, $commitish ) = @_;
92              
93             # BRANCH (default: master)
94             # - check if branch matches, otherwise abort
95             # - git pull
96             # TAG
97             # - git fetch --tags -f
98             # - git checkout TAG
99             # REV
100             # - git fetch --tags -f
101             # - git checkout REVISION
102              
103 13 100       119 $self->log_fatal("Directory $dir exists but is not a Git repository") if !$dir->child('.git')->is_dir;
104              
105 12         1129 my $git = Git::Background->new($dir);
106 12         2059 my $repo = $self->repo;
107              
108             # check that the workspace is from the correct repository
109 12         92 my $origin_f = $git->run( 'config', 'remote.origin.url' )->await;
110 12 100 100     258348 $self->log_fatal("Directory $dir is not a Git repository for $repo") if $origin_f->is_failed || ( $origin_f->stdout )[0] ne $repo;
111              
112 10         9425 my $commitish_id = $commitish->{id};
113 10 100       228 if ( $self->_commitish_is_branch($commitish) ) {
114 9 100       161 if ( !defined $commitish_id ) {
115              
116             # no branch is specified, find the default branch ...
117 7         144 my $origin_head_f = $git->run(qw(symbolic-ref -q --short refs/remotes/origin/HEAD))->await;
118 7 50       150870 if ( $origin_head_f->is_done ) {
119 7         5655 ($commitish_id) = $origin_head_f->stdout;
120 7 50       497 if ( defined $commitish_id ) {
121 7         175 $commitish_id =~ s{ \A origin / }{}xsm;
122             }
123             }
124              
125             # ... or fall back to master
126 7 50 33     207 if ( !defined $commitish_id || $commitish_id eq q{} ) {
127 0         0 $commitish_id = 'master';
128             }
129             }
130              
131             # check if we are on correct branch
132 9         97 my $head_f = $git->run(qw(symbolic-ref -q --short HEAD))->await;
133 9 100 100     197359 $self->log_fatal("Directory $dir is not on branch $commitish_id") if $head_f->is_failed || ( $head_f->stdout )[0] ne $commitish_id;
134             }
135              
136             # check if the workspace is dirty - skip updates for a dirty workspace
137 7 100       5690 if ( $git->run( 'status', '--porcelain' )->stdout ) {
138 2         50173 $self->log( colored( "Git workspace $dir is dirty - skipping checkout", 'yellow' ) );
139 2         3218 $self->_is_dirty(1);
140 2         104 return;
141             }
142              
143             # update the workspace
144 5 100       100620 if ( $self->_commitish_is_branch($commitish) ) {
145 4         156 $self->log("Pulling $repo in $dir");
146 4         5711 $git->run( 'pull', '--ff-only' )->get;
147             }
148             else {
149 1         40 $self->log("Fetching $repo in $dir");
150 1         1274 $git->run( 'fetch', '--tags', '-f' )->get;
151              
152 1         38422 $self->log("Checking out $commitish->{type} $commitish_id in $dir");
153 1         1556 $git->run( 'checkout', $commitish_id )->get;
154             }
155              
156 5         186980 return;
157             }
158              
159             sub _clone {
160 18     18   110 my ( $self, $dir, $commitish ) = @_;
161              
162             # BRANCH (default: master)
163             # - clone --branch branch
164             # TAG
165             # - clone
166             # - checkout tag
167             # REV
168             # - clone
169             # - checkout revision
170              
171 18         1031 my $repo = $self->repo;
172              
173             # clone the repository and checkout the correct commitish
174 18 100       192 if ( $self->_commitish_is_branch($commitish) ) {
175 15 100       254 if ( defined $commitish->{id} ) {
176 2         41 $self->log("Cloning $repo into $dir (branch $commitish->{id})");
177 2         2526 Git::Background->run( 'clone', '--branch', $commitish->{id}, $repo, $dir->stringify )->get;
178             }
179             else {
180 13         238 $self->log("Cloning $repo into $dir");
181 13         17628 Git::Background->run( 'clone', $repo, $dir->stringify )->get;
182             }
183             }
184             else {
185 3         54 $self->log("Cloning $repo into $dir");
186 3         3674 Git::Background->run( 'clone', $repo, $dir->stringify )->get;
187              
188 3         110719 $self->log("Checking out $commitish->{type} $commitish->{id} in $dir");
189 3         6118 Git::Background->run( 'checkout', $commitish->{id}, { dir => $dir } )->get;
190             }
191              
192 18         681180 return;
193             }
194              
195             sub _commitish_is_branch {
196 37     37   27585 my ( $self, $commitish ) = @_;
197              
198 37 100       1022 return 1 if $commitish->{type} eq $BRANCH;
199 7         154 return;
200             }
201              
202             sub _ensure_adequate_git {
203 41     41   220 my ($self) = @_;
204              
205             # check that an adequate Git is available
206 41         605 my $git_version = Git::Background->version;
207 41 100       1763839 $self->log_fatal(q{No 'git' in PATH}) if !defined $git_version;
208              
209             # Find which tag contains a commit: git tag --contains revision
210             #
211             # https://stackoverflow.com/a/6978402/8173111
212             # git status --porcelain, commit 6f15787, September 2009, git 1.7.0
213             # https://github.com/git/git/commit/6f15787181a163e158c6fee1d79085b97692ac2f
214             #
215             # git symbolic-ref --short, commit b8b5290, March 2012, git 1.7.10
216             # https://github.com/git/git/commit/b8b52907e386d064fb0303c08215d7b117d50ee9
217 40 100       4998 $self->log_fatal(q{Your 'git' is too old. At least Git 1.7.10 is needed.}) if !Git::Version::Compare::ge_git( $git_version, '1.7.10' );
218              
219 39         1949 return;
220             }
221              
222             sub _process_options {
223 39     39   616 my ($self) = @_;
224              
225             # verify plugin options
226 39         2822 my $branch = $self->branch;
227 39         2093 my $rev = $self->revision;
228 39         1894 my $tag = $self->tag;
229              
230 39         346 my $num_options = grep { defined } ( $branch, $rev, $tag );
  117         472  
231 39 100       252 $self->log_fatal(q{Only one of branch, revision, or tag can be specified}) if $num_options > 1;
232              
233 35 100       240 return { type => $BRANCH, id => $branch } if defined $branch;
234 30 100       157 return { type => $REVISION, id => $rev } if defined $rev;
235 29 100       259 return { type => $TAG, id => $tag } if defined $tag;
236 24         467 return { type => $BRANCH, id => undef };
237             }
238              
239             sub _run {
240 41     41   150 my ($self) = @_;
241              
242             # check an adequate git is installed
243 41         385 $self->_ensure_adequate_git;
244              
245             # verify the options given to this plugin
246 39         839 my $commitish = $self->_process_options;
247              
248             # clone or update the workspace
249 31         1614 my $dir = path( $self->zilla->root )->child( path( $self->dir ) )->absolute;
250 31 100       8835 if ( $dir->is_dir ) {
251 13         729 $self->_checkout( $dir, $commitish );
252             }
253             else {
254 18         1153 $self->_clone( $dir, $commitish );
255             }
256              
257             # update the push URL
258 25         20017 $self->_update_push_url($dir);
259              
260 25         4206 return;
261             }
262              
263             sub _update_push_url {
264 25     25   1867 my ( $self, $dir ) = @_;
265              
266 25         714 my $git = Git::Background->new($dir);
267              
268             # configure or remove the push url
269 25 100       7902 if ( defined $self->push_url ) {
270 3         186 $git->run( 'remote', 'set-url', '--push', 'origin', $self->push_url )->get;
271             }
272             else {
273 22         223 my $push_url_f = $git->run( 'config', 'remote.origin.pushurl' )->await;
274 22 100       468939 if ( $push_url_f->is_done ) {
275 1         784 my ($push_url) = $push_url_f->stdout;
276              
277 1 50       72 if ( defined $push_url ) {
278 1         18 $git->run( 'remote', 'set-url', '--delete', '--push', 'origin', $push_url )->get;
279             }
280             }
281             }
282              
283 25         103326 return;
284             }
285              
286             __PACKAGE__->meta->make_immutable;
287              
288             1;
289              
290             __END__
291              
292             =pod
293              
294             =encoding UTF-8
295              
296             =head1 NAME
297              
298             Dist::Zilla::Plugin::Git::Checkout - clone and checkout a Git repository
299              
300             =head1 VERSION
301              
302             Version 0.004
303              
304             =head1 SYNOPSIS
305              
306             # in dist.ini:
307             [Git::Checkout]
308             :version = 0.004
309             repo = https://github.com/skirmess/dzil-inc.git
310              
311             =head1 DESCRIPTION
312              
313             This plugin clones, or if it is already cloned, fetches and updates a Git
314             repository.
315              
316             The plugin runs during the initialization phase, which is the same for
317             bundles and plugins. You can check out a Git repository and load bundles
318             or plugins from this repository.
319              
320             # in dist.ini
321             [Git::Checkout]
322             :version = 0.004
323             repo = https://github.com/skirmess/dzil-inc.git
324              
325             ; add the lib directory inside the checked out Git repository to @INC
326             [lib]
327             lib = dzil-inc/lib
328              
329             ; this bundle is run from inside the checked out Git repositories lib
330             ; directory
331             [@BundleFromRepository]
332              
333             Git version 1.7.10 or later is required.
334              
335             =head1 USAGE
336              
337             =head2 branch / revision / tag
338              
339             Available since version 0.004.
340              
341             Specifies what to check out. This can be a branch, a tag or a revision.
342             Only one of these three options can be used.
343              
344             If none is specified it defaults to the branch returned by
345              
346             git symbolic-ref -q --short refs/remotes/origin/HEAD
347              
348             and if that doesn't exist to the branch C<master>.
349              
350             =head2 dir
351              
352             The repositories workspace is checked out into this directory. This defaults
353             to the basename of the repo without the C<.git> suffix.
354              
355             =head2 push_url
356              
357             Allows you to specify a different push url for the repositories origin. One
358             possible scenario would be if you would like to clone via https but push via
359             ssh. This is optional.
360              
361             =head2 repo
362              
363             Specifies the address of the repository to clone. This is required.
364              
365             =head1 SUPPORT
366              
367             =head2 Bugs / Feature Requests
368              
369             Please report any bugs or feature requests through the issue tracker
370             at L<https://github.com/skirmess/Dist-Zilla-Plugin-Git-Checkout/issues>.
371             You will be notified automatically of any progress on your issue.
372              
373             =head2 Source Code
374              
375             This is open source software. The code repository is available for
376             public review and contribution under the terms of the license.
377              
378             L<https://github.com/skirmess/Dist-Zilla-Plugin-Git-Checkout>
379              
380             git clone https://github.com/skirmess/Dist-Zilla-Plugin-Git-Checkout.git
381              
382             =head1 AUTHOR
383              
384             Sven Kirmess <sven.kirmess@kzone.ch>
385              
386             =head1 COPYRIGHT AND LICENSE
387              
388             This software is Copyright (c) 2020-2022 by Sven Kirmess.
389              
390             This is free software, licensed under:
391              
392             The (two-clause) FreeBSD License
393              
394             =head1 SEE ALSO
395              
396             L<Dist::Zilla>, L<lib>
397              
398             =cut
399              
400             # vim: ts=4 sts=4 sw=4 et: syntax=perl