File Coverage

blib/lib/CPAN/InGit/MutableTree.pm
Criterion Covered Total %
statement 122 127 96.0
branch 54 72 75.0
condition 27 41 65.8
subroutine 12 12 100.0
pod 5 6 83.3
total 220 258 85.2


line stmt bran cond sub pod time code
1             package CPAN::InGit::MutableTree;
2             # ABSTRACT: Utility object that represents a Git Tree and pending changes
3             our $VERSION = '0.003'; # VERSION
4              
5              
6 5     5   3389 use Carp;
  5         13  
  5         441  
7 5     5   33 use Moo;
  5         11  
  5         36  
8 5     5   2205 use Git::Raw::Index;
  5         35  
  5         170  
9 5     5   91 use v5.36;
  5         21  
10              
11              
12             has parent => ( is => 'ro', required => 1 );
13             has tree => ( is => 'rw' );
14             has branch => ( is => 'rw' );
15             has _changes => ( is => 'rw' );
16             has has_changes => ( is => 'rw' );
17             has use_workdir => ( is => 'rw' );
18 42     42 1 4009 sub git_repo { shift->parent->git_repo }
19              
20 11     11 0 18024 sub BUILD($self, $args, @) {
  11         28  
  11         26  
  11         26  
21             # branch supplied by name? look it up
22 11 50 66     76 if (defined $self->{branch} && !ref $self->{branch}) {
23 0 0       0 my $b= Git::Raw::Branch->lookup($self->git_repo, $self->{branch}, 1)
24             or croak "No local branch named '$self->{branch}'";
25 0         0 $self->{branch}= $b;
26             }
27             # If branch supplied and tree was not, look up the tree
28 11 100 100     85 if ($self->{branch} && !$self->{tree}) {
29 1         55 $self->{tree}= $self->{branch}->peel('tree');
30             }
31             }
32              
33              
34 42     42 1 3486 sub get_path($self, $path) {
  42         69  
  42         77  
  42         69  
35 42 100       173 if ($self->has_changes) {
36 11 100       51 if (keys $self->_changes->%*) {
37 10         23 my $node= $self->_changes;
38 10         40 my @path= split '/', $path;
39 10         44 my $basename= pop @path;
40 10         29 for (@path) {
41 33 100       87 $node= $node->{$_} if defined $node;
42             }
43 10 100 100     123 return $node->{$basename} if ref $node eq 'HASH' && $node->{$basename};
44             }
45 5 100       22 if ($self->use_workdir) {
46 1         5 my $ent= $self->git_repo->index->find($path);
47 1 50       185 return [ $ent->blob, $ent->mode ]
48             if $ent;
49             }
50             }
51 35 100       196 if ($self->tree) {
52 30 100       741 my $dirent= $self->tree->entry_bypath($path)
53             or return undef;
54 24         3020 return [ $dirent->object, $dirent->file_mode ];
55             }
56 5         44 return undef;
57             }
58              
59              
60 22     22 1 9469 sub set_path($self, $path, $data, %opts) {
  22         40  
  22         53  
  22         36  
  22         45  
  22         39  
61             # Two modes: we can be writing to the working directory and index, or be building a new tree
62             # (which may or may not be connected to a branch)
63 22         152 my $repo= $self->git_repo;
64 22   100     126 my $mode= $opts{mode} // 0100644;
65 22         177 my @path= split m{/+}, $path;
66 22         51 my $basename= pop @path;
67 22 100       81 if ($self->use_workdir) {
68 1         5 my $fullpath= $self->git_repo->workdir;
69             # create missing directories
70 1         5 for (@path) {
71 1         4 $fullpath .= '/'.$_;
72 1 50 50     181 mkdir $fullpath || die "mkdir($fullpath): $!"
73             unless -d $fullpath;
74             }
75 1         5 $fullpath .= '/'.$basename;
76 1 50       8 if (!defined $data) {
77 0         0 unlink($fullpath);
78 0         0 $self->git_repo->index->remove($path);
79             } else {
80             # a shame there's no way to add the blob directly...
81 1 50       21 $data= \$data->content if ref($data)->isa('Git::Raw::Blob');
82             # Write file
83 1         7 _mkfile($fullpath, $data, $mode);
84             # Add to the index
85 1         4 $self->git_repo->index->add_frombuffer($path, $data, $mode);
86             }
87             }
88             else {
89 21   100     111 my $node= ($self->{_changes} //= {});
90 21         52 for (@path) {
91 54   100     196 $node= ($node->{$_} //= {});
92 54 50       226 ref $node eq 'HASH' or die "Can't set '$path'; '$_' is not a directory";
93             }
94             # Content may either be a Blob object or a scalar-ref of bytes
95 21 100       67 if (ref $data eq 'SCALAR') {
96 18         14533 $data= Git::Raw::Blob->create($repo, $$data);
97             }
98 21 50       191 $node->{$basename}= defined $data? [ $data, $mode ] : undef;
99             }
100 22         122 $self->has_changes(1);
101 22   100     74 $self->{_changes} //= {};
102 22         126 $self;
103             }
104              
105 1     1   2 sub _mkfile($path, $scalarref, $mode) {
  1         3  
  1         3  
  1         3  
  1         2  
106 1 50       238 open my $fh, '>', $path or die "open($path): $!";
107 1 50       44 $fh->print($$scalarref) or die "write($path): $!";
108 1 50       25 $fh->close or die "close($path): $!";
109 1 50 0     81 chmod($path, $mode) || die "chmod($path, $mode): $!"
      33        
110             if defined $mode && $mode != 0100644;
111             }
112              
113              
114 9     9 1 4749 sub update_tree($self) {
  9         19  
  9         17  
115             # If using the Index, the index can write the new tree
116 9 100       74 if ($self->use_workdir) {
117 2         7 $self->tree($self->git_repo->index->write_tree);
118             } else {
119 7         24 $self->tree(_assemble_tree($self->git_repo, $self->tree, $self->_changes));
120 7         122 $self->_changes({}); # reset the changes hash
121             }
122             # don't reset has_changes until it has been committed
123             }
124              
125             # merge a hashref of changes into the previous Tree, and return the new Tree
126             # Changes look like:
127             # {
128             # "path1" => {
129             # "filename" => [ $blob, $mode ],
130             # "fname2" => [ $blob, $mode ],
131             # }
132             # }
133 30     30   43 sub _assemble_tree($repo, $tree, $changes) {
  30         40  
  30         39  
  30         31  
  30         38  
134 30 100       243 my $treebuilder= Git::Raw::Tree::Builder->new($repo, ($tree? ($tree) : ()));
135 30         210 for my $name (keys %$changes) {
136 39         67 my $ent= $changes->{$name};
137 39 50       68 if (!defined $ent) {
138 0         0 $treebuilder->remove($name);
139             }
140             else {
141 39 100       128 if (ref $ent eq 'HASH') { # a subdirectory
142 23         83 my $dirent= $treebuilder->get($name);
143 23 100 66     78 my $subdir= $dirent && $dirent->type == Git::Raw::Object::TREE()
144             ? Git::Raw::Tree->lookup($repo, $dirent->id) : undef;
145 23         154 $ent= [ _assemble_tree($repo, $subdir, $ent), 0040000 ];
146             }
147 39         2149 $treebuilder->insert($name, @$ent);
148             }
149             }
150 30         20166 return $treebuilder->write; # returns Git::Raw::Tree
151             }
152              
153              
154 7     7 1 2268 sub commit($self, $message, %opts) {
  7         16  
  7         16  
  7         31  
  7         13  
155 7 50       36 croak "No changes added" unless $self->has_changes;
156 7         40 my $repo= $self->git_repo;
157 7         32 $self->update_tree;
158 7         46 my $branch= $self->branch;
159 7         57 my $cur_sig= $self->parent->new_signature;
160 7   33     81 my $author= $opts{author} // $cur_sig;
161 7   66     46 my $update_head= $self->use_workdir // $opts{update_head};
162 7   33     48 my $committer= $opts{committer} // $cur_sig;
163             my $parents= $self->use_workdir? (
164             # dies on new repo if HEAD doesn't exist yet, in which case no parents
165             eval { [ $self->git_repo->head->target ] } || []
166             )
167             : $branch? [ $self->branch->peel('commit') ]
168 7 50 50     59 : length $opts{create_branch}? [] # fresh branch, no parent commit
    100          
    100          
169             : croak "Can't commit without a branch or use_workdir or option create_branch";
170             # undef final param means don't update HEAD
171 7 50       5744 my $commit= Git::Raw::Commit->create($repo, $message, $author, $committer, $parents, $self->tree, undef)
172             or croak "commit failed";
173 7 100       130 if ($opts{create_branch}) {
    50          
174 6         55 $branch= $repo->branch($opts{create_branch}, $commit);
175 6         4577 $self->branch($branch);
176             } elsif ($branch) {
177             # Update the branch
178 1         837 $branch->target($commit);
179             }
180 7 100 66     1680 $repo->head($branch) if $branch && $update_head;
181             # persist the index state to disk, which clears the staged changes and brings the index
182             # in sync with the commit
183 7 100       487 $repo->index->write if $self->use_workdir;
184 7         36 $self->has_changes(0);
185 7         117 return $commit;
186             }
187              
188             1;
189              
190             __END__