File: git.rakumod

package info (click to toggle)
raku-zef 0.13.8-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 664 kB
  • sloc: perl: 22; makefile: 8
file content (270 lines) | stat: -rw-r--r-- 10,733 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
use Zef;
use Zef::Utils::URI;

class Zef::Service::Shell::git does Fetcher does Extractor does Probeable does Messenger {

    =begin pod

    =title class Zef::Service::Shell::git

    =subtitle A git based implementation of the Fetcher and Extractor interfaces

    =head1 Synopsis

    =begin code :lang<raku>

        use Zef;
        use Zef::Service::Shell::git;

        my $git = Zef::Service::Shell::git.new;

        # Fetch the git repository
        my $tag-or-sha = "@v0.9.0";
        my $source     = "https://github.com/ugexe/zef.git{$tag-or-sha}";
        my $save-to    = $*CWD.child("backup_dir{$tag-or-sha}"); # must include tag in save path currently :/
        my $saved-to   = $git.fetch($source, $save-to);

        say "Zef META6 from HEAD: ";
        say $saved-to.child("META6.json").slurp;

        # Extract the repository
        my $extract-to   = $*CWD.child("extracted_backup_dir");
        my $extracted-to = $git.extract($saved-to, $extract-to);

        say "Zef META6 from older $tag-or-sha: ";
        say $extracted-to.dir.first({ .basename eq "META6.json" }).slurp;

    =end code

    =head1 Description

    C<Fetcher> and C<Extractor> class for handling git URIs using the C<git> command.

    You probably never want to use this unless its indirectly through C<Zef::Fetch> or C<Zef::Extractor>;
    handling files and spawning processes will generally be easier using core language functionality. This
    class exists to provide the means for cloning git repos and checking out revisions using the C<Fetcher>
    and C<Extractor> interfaces that the e.g. http/tar fetching/extracting adapters use.

    =head1 Methods

    =head2 method probe

        method probe(--> Bool:D)

    Returns C<True> if this module can successfully launch the C<git> command.

    =head2 method fetch-matcher

        method fetch-matcher(Str() $uri --> Bool:D)

    Returns C<True> if this module knows how to fetch C<$uri>, which it decides based on if a parsed C<$uri>
    ends with C<.git> (including local directories) and starts with any of C<http> C<git> C<ssh>.

    =head2 method extract-matcher

        method extract-matcher(Str() $uri --> Bool:D) 

    Returns C<True> if this module knows how to extract C<$uri>, which it decides based on if a parsed C<$uri>
    looks like a directory and if C<git status> can successfully be run from that directory.

    =head2 method fetch

        method fetch(Str() $uri, IO() $save-to --> IO::Path)

    Fetches the given C<$uri> via C<git clone $uri $save-to>, or via C<git pull> if C<$save-to> is an existing git repo.

    On success it returns the C<IO::Path> where the data was actually saved to. On failure it returns C<Nil>.

    =head2 method extract

        method extract(IO() $repo-path, IO() $extract-to)

    Extracts the given C<$repo-path> from the file system to C<$save-to> via C<git checkout ...>.

    On success it returns the C<IO::Path> where the data was actually extracted to. On failure it returns C<Nil>.

    =head2 method ls-files

        method ls-files(IO() $repo-path --> Array[Str])

    On success it returns an C<Array> of relative paths that are available to be extracted from C<$repo-path>.

    =end pod


    #| This is for overriding the uri scheme used for git, i.e. force https:// over git://
    has Str $.scheme;

    #| Return true if the `git` command is available to use
    method probe(--> Bool:D) {
        state $probe = try { run('git', '--help', :!out, :!err).so };
    }

    #| Return true if this Fetcher understands the given uri/path
    method fetch-matcher(Str() $uri --> Bool:D) {
        # $uri may contain non-uri-standard, git specific, uri parts (like a trailing @tag)
        my $clean-uri = self!repo-url($uri).lc;
        return False unless $clean-uri.ends-with('.git');
        return so <git http ssh>.first({ $clean-uri.starts-with($_) });
    }

    #| Return true if this Extractor understands the given uri/path
    method extract-matcher(Str() $uri --> Bool:D) {
        return False unless $uri.IO.d;
        # When used to 'extract' we want to ensure the path is a git repository (which may use a non-standard .git dir location)
        my $proc = Zef::zrun('git', 'status', :!out, :!err, :cwd($uri));
        return $proc.so;
    }

    #| Fetch the given url.
    #| First attempts to clone the repository, but if it already exists (or fails) it attempts to pull down new changes
    method fetch(Str() $uri, IO() $save-as --> IO::Path) {
        return self!clone(self!repo-url($uri), $save-as) || self!pull($save-as);
    }

    #| Extracts the given path.
    #| For a git repo extraction is equivalent to checking out a specific revision and copying it to separate location
    method extract(IO() $repo-path, IO() $extract-to) {
        die "target repo directory {$repo-path.absolute} does not contain a .git/ folder"
            unless $repo-path.child('.git').d;

        my $sha1 = self!rev-parse(self!fetch($repo-path)).head;
        die "target repo directory {$repo-path.absolute} failed to locate checkout revision"
            unless $sha1;

        my $checkout-to = $extract-to.child($sha1);
        die "target repo directory {$extract-to.absolute} does not exist and could not be created"
            unless ($checkout-to.e && $checkout-to.d) || mkdir($checkout-to);

        return self!checkout($repo-path, $checkout-to, $sha1);
    }

    #| Returns an array of strings, where each string is a relative path representing a file that can be extracted from the given $repo-path
    method ls-files(IO() $repo-path --> Array[Str]) {
        die "target repo directory {$repo-path.absolute} does not contain a .git/ folder"
            unless $repo-path.child('.git').d;

        my $passed;
        my $output = Buf.new;
        react {
            my $cwd := $repo-path.absolute;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', 'ls-tree', '-r', '--name-only', self!checkout-name($repo-path));
            whenever $proc.stdout(:bin) { $output.append($_) }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        my @extracted-paths = $output.decode.lines;

        my Str @results = $passed ?? @extracted-paths.grep(*.defined) !! ();
        return @results;
    }

    #| On success returns an IO::Path to where a `git clone ...` has put files
    method !clone($url, IO() $save-as --> IO::Path) {
        die "target download directory {$save-as.absolute} does not exist and could not be created"
            unless $save-as.d || mkdir($save-as);

        my $passed;
        react {
            my $cwd := $save-as.parent;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', 'clone', $url, $save-as.basename, '--quiet');
            whenever $proc.stdout(:bin) { }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        return ($passed && $save-as.child('.git').d) ?? $save-as !! Nil;
    }

    #| Does a `git pull` on an existing local git repo
    method !pull(IO() $repo-path --> IO::Path) {
        die "target download directory {$repo-path.absolute} does not contain a .git/ folder"
            unless $repo-path.child('.git').d;

        my $passed;
        react {
            my $cwd := $repo-path.absolute;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', 'pull', '--quiet');
            whenever $proc.stdout(:bin) { }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        return $passed ?? $repo-path !! Nil;
    }

    #| Does a `git fetch` on an existing local git repo. Not really related to self.fetch(...)
    method !fetch(IO() $repo-path --> IO::Path) {
        die "target download directory {$repo-path.absolute} does not contain a .git/ folder"
            unless $repo-path.child('.git').d;

        my $passed;
        react {
            my $cwd := $repo-path.absolute;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', 'fetch', '--quiet');
            whenever $proc.stdout(:bin) { }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        return $passed ?? $repo-path !! Nil;
    }

    #| Does a `git checkout ...`, allowing git source urls to have e.g. trailing @tag
    method !checkout(IO() $repo-path, IO() $extract-to, $target --> IO::Path) {
        my $passed;
        react {
            my $cwd := $repo-path.absolute;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', '--work-tree', $extract-to, 'checkout', $target, '.');
            whenever $proc.stdout(:bin) { }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        return $passed ?? $extract-to !! Nil;
    }

    #| Does a `git rev-parse ...` (used to get a sha1 for saving a copy of a specific checkout)
    method !rev-parse(IO() $save-as --> Array[Str]) {
        die "target repo directory {$save-as.absolute} does not contain a .git/ folder"
            unless $save-as.child('.git').d;

        my $passed;
        my $output = Buf.new;
        react {
            my $cwd := $save-as.absolute;
            my $ENV := %*ENV;
            my $proc = Zef::zrun-async('git', 'rev-parse', self!checkout-name($save-as));
            whenever $proc.stdout(:bin) { $output.append($_) }
            whenever $proc.stderr(:bin) { }
            whenever $proc.start(:$ENV, :$cwd) { $passed = $_.so }
        }

        my @extracted-refs = $output.decode.lines;

        my Str @results = $passed ?? @extracted-refs.grep(*.defined) !! ();
        return @results;
    }

    #| Git URI parser / transformer
    #| Handles overriding the uri $.scheme, and removing parts of the URI that e.g. `git clone ...` wouldn't understand
    #| (like a trailing @tag on the uri)
    method !repo-url($url --> Str) {
        my $uri = uri($!scheme ?? $url.subst(/^\w+ '://'/, "{$!scheme}://") !! $url) || return False; #'
        my $reconstructed-uri = ($uri.scheme // '') ~ '://' ~ ($uri.user-info ?? "{$uri.user-info}@" !! '') ~ ($uri.host // '') ~ ($uri.path // '').subst(/\@.*[\/|\@|\?|\#]?$/, '');
        return $reconstructed-uri;
    }

    #| Given a $url like http://foo.com/project.git@v1 or ./project.git@v1 will return 'v1'
    method !checkout-name($url --> Str) {
        my $uri      = uri($url) || return False;
        my $checkout = ($uri.path // '').match(/\@(.*)[\/|\@|\?|\#]?/)[0];
        return $checkout ?? $checkout.Str !! 'HEAD';
    }
}