gmerge and pick-to-branch: further tweak output
[tools.git] / review-tools / ghlink
1 #!/usr/bin/env perl
2 #
3 # Copyright 2019 The OpenSSL Project Authors. All Rights Reserved.
4 #
5 # Licensed under the Apache License 2.0 (the "License").  You may not use
6 # this file except in compliance with the License.  You can obtain a copy
7 # in the file LICENSE in the source distribution or at
8 # https://www.openssl.org/source/license.html
9
10 #
11 # ghlink - convert repository locations into GitHub links
12 #
13
14 use warnings;
15 use strict;
16
17 use File::Basename;
18 use Getopt::Long;
19 use Pod::Usage;
20
21 my $progname=basename($0);
22
23 my $repo = "https://github.com/openssl/openssl";
24 my $tree = "$repo/tree";
25 my $blob = "$repo/blob";
26
27 my $regex_name = "[[:alpha:]._-]+[[:alnum:]._-]*";
28 my $regex_revision = "[[:alnum:]._-]+";
29 my $regex_path = "(?:${regex_name}/)*${regex_name}";
30 my $regex_lineno = "[0-9]+";
31
32 my $style = 'default';
33
34 my $replace = \&link;
35
36 my $markdown = 0;
37 my $permanent = 0;
38 my $list = 0;
39 my $help = 0;
40 my $man = 0;
41
42 GetOptions('markdown|m'        => \$markdown,
43            'permanent|p'       => \$permanent,
44            'list|l'            => \$list,
45            'help|h'            => \$help,
46            'man'               => \$man)
47     or die "Errors in command line arguments\n";
48
49 if ($help) {
50     pod2usage(-exitval => 0,
51               -verbose => 1);
52 }
53 if ($man) {
54     pod2usage(-exitval => 0,
55               -verbose => 2);
56 }
57
58 if ($markdown) {
59     $replace = \&markdown
60 }
61
62
63 my $remotes = `git remote -v`;
64 if ($? != 0 or $remotes !~ "openssl\.git") {
65     die "Current directory does not belong to an OpenSSL git repository";
66 }
67
68 my $curr_branch = `git rev-parse --abbrev-ref HEAD`;
69 chomp $curr_branch;
70
71 my $prefix = `git rev-parse --show-prefix`;
72 chomp $prefix;
73
74
75 # some results are cached for efficiency reasons
76
77 my %commits;
78 my %urls;
79
80 sub check_url {
81     my ($match, $revision, $path, $lineno) = @_;
82
83     if (!defined($revision)) {
84         $revision = $curr_branch;
85     }
86
87     my $abbrev_commit = ($permanent) ? "" : "--abbrev-commit";
88
89     if (!defined($commits{$revision})) {
90         my $c = `git rev-list $abbrev_commit -1 $revision -- 2>/dev/null`;
91         chomp $c;
92
93         if ($? != 0) {
94             $commits{$revision} = "";
95         } elsif ($permanent) {
96             # always use the commit id if --permanent was specified
97             $commits{$revision} = $c;
98         } else {
99             # if a branch name was specified, use if it exists remotely
100             # otherwise, use the commit id
101             `git ls-remote --exit-code $repo --heads refs/heads/$revision`;
102             $commits{$revision} = ($? == 0) ? $revision : $c;
103         }
104     }
105
106     $revision = $commits{$revision};
107
108     if (!$revision) {
109         return "";
110     }
111
112     my $gitpath = "$revision:$prefix$path";
113
114     if (!defined($urls{$gitpath})) {
115         # create urls only for objects in the local repository
116         `git rev-parse $gitpath 2>/dev/null`;
117         if ($? != 0) {
118             $urls{$gitpath} = "";
119         } else {
120             $urls{$gitpath} = "$blob/$revision/$prefix$path";
121         }
122     }
123
124     my $url = $urls{$gitpath};
125
126     if (!$url) {
127         return "";
128     }
129
130     return defined($lineno) ? "$url#L$lineno" : "$url";
131 }
132
133 sub link {
134     my ($match, $revision, $path, $lineno) = @_;
135
136     my $url = check_url($match, $revision, $path, $lineno);
137
138     if (!$url) {
139         return $match;
140     }
141
142     return $url;
143 }
144
145 sub markdown {
146     my ($match, $revision, $path, $lineno) = @_;
147
148     my $url = check_url($match, $revision, $path, $lineno);
149
150     if (!$url) {
151         return $match;
152     }
153
154     return "[$match]($url)";
155 }
156
157 my $regex = qr/(?:(${regex_revision}):)?(${regex_path}):(${regex_lineno})?:?/;
158
159 if ($list) {
160     # list mode: print github links for all matched locations (and discard the rest)
161     while (<>) {
162         while (/$regex/g) {
163             my $found = $replace->($&,$1,$2,$3);
164             if ($found ne $&) {
165                 print ("$found\n");
166             }
167         }
168     }
169 } else {
170     # replace mode: replace all matched locations in the text with github links
171     while (<>) {
172         s/$regex/$replace->($&,$1,$2,$3)." "/eg;
173         print;
174     }
175 }
176
177
178 __END__
179
180 =head1 NAME
181
182 ghlink - convert repository locations into GitHub links
183
184 =head1 SYNOPSIS
185
186   ghlink [<option>...] [<file>...]
187
188 Concatenate the given file(s) to standard output, converting repository
189 locations into GitHub links. If no file is given, read from stdin.
190
191
192 =head1 DESCRIPTION
193
194 The most common usage of ghlink is to add it as an output filter for commands which
195 produce output referring to locations in the repository. A location can be of roughly
196 the following form:
197
198     [<branch-or-commit>:]<path>:[<lineno>[:]]
199
200 Examples of such commands are git-grep or find:
201
202   git grep [-n] [-e] <expression>  ...  |  ghlink [<option>...]
203
204   find -name <pattern> ...  |  ghlink [<option>...]
205
206 Alternatively, the output of the commands can be redirected into files which then
207 can be specified as commandline arguments for ghlink.
208
209   ghlink <file>...
210
211 The ghlink filter recognizes the locations and converts them into links to the
212 OpenSSL GitHub repository, depending on the given options. It tries hard to provide
213 only valid locations. For that reason, ghlink requires to be run from within the
214 working copy. It is allowed to run ghlink from subdirectories, because it takes
215 its relative position into account. Branch names in the output (like from git-grep)
216 are recognized, and if they are missing, the current branch is assumed. When creating
217 the link, ghlink attempts to preserve the branch name, provided the branch exists on
218 the remote repository too (e.g. master, OpenSSL_x_y_z-stable). If the branch does not
219 exist remotely (or if the --permanent option was specified), the branch is resolved
220 locally to a commit-id.
221
222 =head1 OPTIONS
223
224 =over 4
225
226 =item B<--help>
227
228 Print a brief help message and exit.
229
230 =item B<--man>
231
232 Print the manual page and exit.
233
234 =item B<--markdown> | B<-m>
235
236 Convert the locations to links in markdown syntax, with the location in square brackets,
237 followed by the link in parentheses.
238
239 =item B<--permanent> | B<-p>
240
241 Always resolve branches to (unabbreviated) commit-ids for printing the link.
242
243 =item B<--list> | B<-l>
244
245 List only the links of all locations found, and omit the resto of th text. This option
246 is useful together with the --permanent option to create GitHub permalinks, see EXAMPLES
247 section below.
248
249 =back
250
251 =head1 EXAMPLES
252
253 =over 4
254
255 =item B<~/src/openssl$ git grep -n expression | ghlink>
256
257 Search for an expression locally in the repository and browse the results directly
258 on GitHub by clicking on the URLs using <ctrl>-<left-mouse> (or whatever key binding
259 the terminal supports).
260
261 =item B<~/src/openssl$ git grep -n expression | ghlink --markdown  [--permanent]>
262
263 Produces output optimized for posting the result on GitHub as an issue or
264 pull request comment. The locations show up as markdown links in the
265 original grep output format.
266
267 =item B<~/src/openssl$ git grep -n expression | ghlink --permanent --list>
268
269 Produces output optimized for posting the result on GitHub as an issue or
270 pull request comment. The locations show up as permalink text boxes, which
271 is the reason why --list was output only the permalinks and omit the text.
272
273 =item B<~/src/openssl$ ghlink --markdown {gdb.log,asan.log,tsan.log,...}>
274
275 The ghlink tool also works nicely with call stacks produced by gdb, AddressSanitizer,
276 or ThreadSanitizer and facilitates viewing the stack frames directly in the source.
277 It can either be applied directly as output filter or subsequently on the saved
278 log files.
279
280 =back
281
282 =cut