File Coverage

blib/lib/Git/Release/Branch.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Git::Release::Branch;
2 1     1   2722 use 5.12.0;
  1         3  
  1         54  
3 1     1   503 use Moose;
  0            
  0            
4             use File::Spec;
5             use File::Path qw(mkpath);
6             use Git;
7              
8             has name => ( is => 'rw' , isa => 'Str' );
9              
10              
11             # ref: remotes/origin/branch_name (remote)
12             # ref: branch_name (local)
13              
14             has ref => ( is => 'rw' );
15              
16             has tracking_ref => ( is => 'rw', isa => 'Str' );
17              
18             has manager => ( is => 'rw' );
19              
20             has remote => ( is => 'rw' );
21              
22             has is_deleted => ( is => 'rw' );
23              
24             sub BUILD {
25             my ($self,$args) = @_;
26             unless( $args->{ref} ) {
27             $args->{ref} = $args->{name} if $args->{name};
28             } else {
29             $args->{ref} =~ s{^refs/}{}; # always strip refs prefix
30             }
31              
32             $args->{tracking_ref} =~ s{^refs/}{} if $args->{tracking_ref};
33              
34             unless( $args->{remote} ) {
35             my $remote_name = $self->parse_remote_name($args->{ref});
36             $self->remote($remote_name);
37             }
38             unless( $args->{name} ) {
39             my $name = $self->strip_remote_prefix( $args->{ref} );
40             $args->{name} = $name;
41             $self->name($name);
42             }
43             $self->load_tracking_ref;
44             return $args;
45             }
46              
47             sub load_tracking_ref {
48             my ($self) = @_;
49             # remote tracking ref
50             unless ( $self->tracking_ref ) {
51             # try to get tracking ref
52             my %tracking = $self->manager->tracking_list;
53             my $ref = $tracking{ $self->name }
54             if $self->name && defined $tracking{ $self->name };
55             $ref =~ s{^refs/}{} if $ref;
56             $self->tracking_ref( $ref ) if $ref;
57             }
58             }
59              
60              
61             =head2 parse_remote_name
62              
63             Parse remote name from ref, like:
64              
65             remotes/origin/branch_name
66              
67             =cut
68              
69             sub parse_remote_name {
70             my ($self,$ref) = @_;
71             return unless $ref;
72             my $new = $ref;
73             chomp $new;
74             my ($remote) = ($new =~ m{^remotes/([^/]+?)\/});
75             return $remote;
76             }
77              
78             =head2 strip_remote_prefix
79              
80             Strip remotes prefix from branch ref string
81              
82             remotes/origin/branch_name
83              
84             To
85              
86             origin/branch_name
87              
88             =cut
89              
90             sub strip_remote_prefix {
91             my ($self,$ref) = @_;
92             my $new = $ref;
93             $new =~ s{^remotes\/([^/]+?)\/}{};
94             return $new;
95             }
96              
97              
98              
99              
100             =head2 prefix
101              
102             Get branch prefix name, for remote branch, return remotes/{prefix}
103              
104             For local branch, return {prefix}
105              
106             =cut
107              
108             sub prefix {
109             my $self = shift;
110             my ($prefix) = ($self->name =~ m{^([^/]*)/}i);
111             return $prefix;
112             }
113              
114             sub is_feature {
115             my $self = shift;
116             return $self->prefix eq 'feature';
117             }
118              
119             sub is_ready {
120             my $self = shift;
121             return $self->prefix eq 'ready';
122             }
123              
124             sub is_local { return ! $_[0]->is_remote; }
125              
126             sub is_remote { return $_[0]->ref =~ m{^remotes/}; }
127              
128             sub remote_name {
129             my $self = shift;
130             return if $self->remote;
131             if($self->is_remote) {
132             return $self->parse_remote_name( $self->ref );
133             } elsif( $self->tracking_ref ) {
134             return $self->parse_remote_name( $self->tracking_ref );
135             }
136             }
137              
138              
139             # return remote object
140             sub get_remote {
141             my $self = shift;
142             return $self->manager->remote->get( $self->remote_name );
143             }
144              
145             sub remote_tracking_branch {
146             my $self = shift;
147             return $self->manager->branch->new_branch(ref => $self->tracking_ref);
148             }
149              
150             sub has_tracking_ref {
151             return $_[0]->tracking_ref ? 1 : 0;
152             }
153              
154              
155             =head2 create
156              
157             create branch
158              
159             =cut
160              
161             sub create {
162             my ($self,%args) = @_;
163             my @args = qw(branch);
164              
165             # git branch --set-upstream develop origin/develop
166             CORE::push @args, '--set-upstream' if $args{upstream};
167             CORE::push @args, $self->name;
168             CORE::push @args, $args{from} || 'master';
169              
170             $self->manager->repo->command(@args);
171             return $self;
172             }
173              
174             # options:
175             #
176             # ->delete( force => 1 , remote => 1 );
177             # ->delete( force => 1 , remote => ['origin','github'] );
178             # ->delete( force => 1 , remote => 'github' );
179              
180             sub delete {
181             my ($self,%args) = @_;
182             if( $args{remote} ) {
183             if( ref($args{remote}) eq 'ARRAY' ) {
184             $self->manager->repo->command( 'push' , $_ , ':' . $self->name )
185             for @{ $args{remote} };
186             }
187             elsif( $args{remote} == 1 && $self->remote_name ) {
188             $self->manager->repo->command( 'push' , $self->remote_name , ':' . $self->name );
189             }
190             else {
191             $self->manager->repo->command( 'push' , ($args{remote}) , ':' . $self->name );
192             }
193             }
194             elsif( $args{local} || $self->is_local ) {
195             $self->manager->repo->command( 'branch' , $args{force} ? '-D' : '-d' , $self->ref );
196             }
197             elsif( $self->is_remote ) {
198             $self->manager->repo->command( 'push', $self->remote, ':' . $self->name );
199             }
200             $self->is_deleted(1);
201             return $self;
202             }
203              
204              
205             =head2 local_rename
206              
207             Rename branch locally.
208              
209             =cut
210              
211             sub local_rename {
212             my ($self,$new_name,%args) = @_;
213             if( $self->is_local ) {
214             if( $args{force} ) {
215             $self->manager->repo->command( 'branch','-m',$self->name,$new_name);
216             } else {
217             $self->manager->repo->command( 'branch','-M',$self->name,$new_name);
218             }
219             $self->name($new_name);
220             $self->update_ref($new_name);
221             }
222             }
223              
224              
225             =head2 update_ref
226              
227             update_ref by branch name
228              
229             =cut
230              
231             sub update_ref {
232             my ($self,$name) = @_;
233             if( $self->is_remote ) {
234             $self->ref( join '/','remotes',$self->remote,$name );
235             } elsif( $self->is_local ) {
236             $self->ref( $name );
237             }
238             }
239              
240             sub rename {
241             my ($self,$new_name,%args) = @_;
242             if( $self->is_remote ) {
243             # if local branch is found, then checkout it
244             # if not found, then checkout remote tracking branch
245             my $local = $self->manager->branch->find_local_branches($self->name);
246             $local = $self->checkout unless $local;
247             $local->pull(
248             remote => $self->remote,
249             no_edit => 1,
250             fast_forward => 1
251             );
252             $local->delete( remote => 1 );
253              
254             $local->local_rename( $new_name , %args );
255              
256             $local->push( $self->remote );
257             $self->name($new_name);
258             $self->update_ref($new_name);
259             }
260             elsif( $self->is_local && $self->tracking_ref ) {
261             $self->delete( remote => 1 );
262             $self->local_rename( $new_name , %args );
263             $self->push( $self->remote_name ); # push to tracking remote
264             }
265             elsif( $self->is_local ) {
266             $self->local_rename($new_name,%args);
267             }
268             }
269              
270             sub checkout {
271             my $self = shift;
272             if( $self->is_remote ) {
273             # find local branch to checkout if the branch exists
274             my $local = $self->manager->branch->find_local_branches($self->name);
275             if( $local ) {
276             $self->manager->repo->command( 'checkout' , $local->name );
277             return $local;
278             } else {
279             $self->manager->repo->command( 'checkout' , '-t' , $self->ref , '-b' , $self->name );
280             return $self->manager->branch->new_branch( ref => $self->name ); # local branch instance
281             }
282             }
283             elsif( $self->is_local && $self->tracking_ref ) {
284             my $local = $self->manager->branch->find_local_branches($self->name);
285             if( $local ) {
286             $self->manager->repo->command( 'checkout' , $local->name );
287             } else {
288             $self->manager->repo->command( 'checkout' , '-t' , $self->tracking_ref , '-b' , $self->name );
289             }
290             return $self;
291             }
292             elsif( $self->is_local ) {
293             $self->manager->repo->command( 'checkout' , $self->name );
294             }
295             }
296              
297             sub merge {
298             my ($self,$b, %args) = @_;
299             my @args = ( 'merge' );
300             CORE::push @args, '--ff' if $args{fast_forward};
301             CORE::push @args, '--edit' if $args{edit};
302             CORE::push @args, '--no-edit' if $args{no_edit};
303             CORE::push @args, '--squash' if $args{squash};
304             CORE::push @args, '--quiet' if $args{quiet};
305             CORE::push @args, ref($b) eq 'Git::Release::Branch' ? $b->ref : $b;
306             return $self->manager->repo->command( @args );
307             }
308              
309             sub rebase_from {
310             my ($self,$from) = @_;
311             if( ! ref($from) ) {
312             $from = $self->manager->_new_branch( ref => $from );
313             }
314             my @ret = $self->manager->repo->command( 'rebase' , $from->name , $self->name );
315             return @ret;
316             }
317              
318             sub push {
319             my ($self,$remote,%args) = @_;
320             $remote ||= $self->remote;
321             die "remote name is requried." unless $remote;
322             my @args = ('push');
323              
324             # git push --set-upstream origin develop
325             CORE::push @args, '--set-upstream' if $args{upstream};
326             CORE::push @args, '--tags' if $args{tags};
327             CORE::push @args, '--all' if $args{all};
328             CORE::push @args, $remote;
329             CORE::push @args, $self->name;
330             $self->manager->repo->command(@args);
331              
332             if( $args{upstream} ) {
333             # update tracking_ref
334             # eg: refs/remotes/origin/branch
335             $self->tracking_ref(join '/','refs','remotes',$remote,$self->name);
336             }
337              
338             }
339              
340             sub push_to_remotes {
341             my $self = shift;
342             $self->push($_)
343             for $self->manager->remote->list;
344             }
345              
346              
347              
348             # Remove remote tracking branches
349             # if self is a local branch, we can check if it has a remote branch
350             sub delete_remote_branches {
351             my $self = shift;
352             my $name = $self->name;
353             my @remotes = $self->manager->repo->command( 'remote' );
354             my @rbs = $self->manager->list_remote_branches;
355              
356             for my $remote ( @remotes ) {
357             # has tracking branch at remote ?
358             if( grep m{$remote/$name},@rbs ) {
359             $self->manager->repo->command( 'push' , $remote , '--delete' , $self->name );
360             }
361             }
362             }
363              
364             sub replace_prefix {
365             my ($self,$new_prefix,%args) = @_;
366             if ($self->prefix) {
367             my $new_name = $self->name;
368             $new_name =~ s{^([^/]*)/}{$new_prefix/};
369             $self->rename( $new_name, %args );
370             }
371             }
372              
373             sub remove_prefix {
374             my ($self,%args) = @_;
375             if ($self->prefix) {
376             my $new_name = $self->name;
377             $new_name =~ s{^([^/]*)/}{};
378             $self->rename( $new_name, %args );
379             }
380             }
381              
382             sub prepend_prefix {
383             my ($self,$prefix,%args) = @_;
384             return if $self->prefix && $self->prefix eq $prefix;
385             my $new_name = join '/',$prefix,$self->name;
386             $self->rename($new_name,%args);
387             }
388              
389             sub pull {
390             my ($self,%args) = @_;
391             my @a = ('pull');
392             CORE::push @a, '--rebase' if $args{rebase};
393             CORE::push @a, '--quiet' if $args{quiet};
394             CORE::push @a, '--no-commit' if $args{no_commit};
395             CORE::push @a, '--commit' if $args{commit};
396             CORE::push @a, '--ff' if $args{fast_forward};
397             CORE::push @a, '--edit' if $args{edit};
398             CORE::push @a, '--no-edit' if $args{no_edit};
399             CORE::push @a, '--squash' if $args{squash};
400             CORE::push @a, ($args{remote} || $self->remote_name || 'origin');
401             CORE::push @a, ($args{name} || $self->name);
402             return $self->manager->repo->command(@a);
403             }
404              
405             sub move_to_ready {
406             my $self = shift;
407             my $name = $self->name;
408             my $prefix = $self->manager->config->ready_prefix;
409             return if $self->prefix && $self->prefix eq $prefix;
410              
411             my $new_name = $prefix . '/' . $name;
412             say "Moving branch @{[ $self->name ]} to " , $new_name;
413             $self->prepend_prefix( $prefix );
414             return $self;
415             }
416              
417             sub move_to_released {
418             my $self = shift;
419             my $name = $self->name;
420             my $prefix = $self->manager->config->released_prefix;
421             return if $self->prefix && $self->prefix eq $prefix;
422             return if $self->prefix ne 'ready'; # if it's not with ready prefix, do not move to released/
423              
424             say "Moving branch @{[ $self->name ]} to released state.";
425             $self->replace_prefix( $prefix );
426             return $self;
427             }
428              
429             sub get_doc_path {
430             my $self = shift;
431             my $docname = $self->name;
432             return if $self->name eq 'HEAD';
433              
434             $docname =~ s/^@{[ $self->manager->config->released_prefix ]}//;
435             $docname =~ s/^@{[ $self->manager->config->ready_prefix ]}//;
436              
437             my $ext = $self->manager->config->branch_doc_ext;
438              
439             my $dir = File::Spec->join( $self->manager->repo->wc_path , $self->manager->config->branch_doc_path );
440             mkpath [ $dir ] if ! -e $dir ;
441             return File::Spec->join( $dir , "$docname.$ext" );
442             }
443              
444             sub default_doc_template {
445             my $self = shift;
446             return <<"END";
447             @{[ $self->name ]}
448             ======
449              
450             REQUIREMENT
451             ------------
452              
453             SYNOPSIS
454             ------------
455              
456             PLAN
457             ------------
458              
459             KNOWN ISSUES
460             ------------
461              
462             END
463             }
464              
465             sub init_doc {
466             my $self = shift;
467             my $doc_path = $self->get_doc_path;
468             return unless $doc_path;
469              
470              
471             print "Initializing branch documentation.\n";
472             open my $fh , ">" , $doc_path;
473             print $fh $self->default_doc_template;
474             close $fh;
475              
476             $self->edit_doc;
477             print "Done.\n";
478             }
479              
480             sub edit_doc {
481             my $self = shift;
482             my $doc_path = $self->get_doc_path;
483             return unless $doc_path;
484              
485             # XXX:
486             # launch editor to edit doc
487             # my $bin = $ENV{EDITOR} || 'vim';
488             # system(qq{$bin $doc_path}) == 0
489             # or die "System failed: $?";
490              
491             }
492              
493             sub print_doc {
494             my $self = shift;
495             my $doc_path = $self->get_doc_path;
496             return unless $doc_path;
497              
498             print "Branch doc path: $doc_path\n";
499              
500             # doc doesn't exists
501             unless(-e $doc_path ){
502             print "Branch doc $doc_path not found.\n";
503             $self->init_doc;
504             return;
505             }
506              
507             if($doc_path =~ /\.pod$/) {
508             system("pod2text $doc_path");
509             }
510             else {
511             open my $fh , "<" , $doc_path;
512             local $/;
513             my $content = <$fh>;
514             close $fh;
515             print "===================\n";
516             print $content , "\n";
517             print "===================\n";
518             }
519             }
520              
521              
522              
523             1;
524             __END__
525             =head1
526              
527             =head2 SYNOPSIS
528              
529             my $branch = $manager->branch->current;
530             my $develop = $manager->branch->new_branch( 'develop' )->create( from => 'master' );
531              
532             $develop->delete;
533             $develop->push;
534             $develop->push('origin');
535             $develop->push('github');
536             $develop->push_to_remotes;
537              
538             =head3 delete_remote_branches
539              
540             =head3 move_to_ready
541              
542             =head3 move_to_released
543              
544             =cut