renamed gitpub to gitprep
|
1 |
package Gitprep::Git; |
copy gitweblite soruce code
|
2 |
use Mojo::Base -base; |
3 | ||
4 |
use Carp 'croak'; |
|
5 |
use File::Find 'find'; |
|
6 |
use File::Basename qw/basename dirname/; |
|
7 |
use Fcntl ':mode'; |
|
8 | ||
9 |
# Encode |
|
10 |
use Encode qw/encode decode/; |
|
11 |
sub enc { |
|
12 |
my ($self, $str) = @_; |
|
13 |
|
|
14 |
my $enc = $self->encoding; |
|
15 |
|
|
16 |
return encode($enc, $str); |
|
17 |
} |
|
18 | ||
19 |
sub dec { |
|
20 |
my ($self, $str) = @_; |
|
21 |
|
|
22 |
my $enc = $self->encoding; |
|
23 |
|
|
24 |
my $new_str; |
|
25 |
eval { $new_str = decode($enc, $str) }; |
|
26 |
|
|
27 |
return $@ ? $str : $new_str; |
|
28 |
} |
|
29 | ||
30 |
# Attributes |
|
31 |
has 'bin'; |
|
32 |
has 'search_dirs'; |
|
33 |
has 'search_max_depth'; |
|
34 |
has 'encoding'; |
|
35 |
has 'text_exts'; |
|
36 | ||
37 |
sub blob_mimetype { |
|
38 |
my ($self, $fh, $file) = @_; |
|
39 | ||
40 |
return 'text/plain' unless $fh; |
|
41 |
|
|
42 |
# MIME type |
|
43 |
my $text_exts = $self->text_exts; |
|
44 |
for my $text_ext (@$text_exts) { |
|
45 |
my $ext = quotemeta($text_ext); |
|
46 |
return 'text/plain' if $file =~ /\.$ext$/i; |
|
47 |
} |
|
48 |
if (-T $fh) { return 'text/plain' } |
|
49 |
elsif (! $file) { return 'application/octet-stream' } |
|
50 |
elsif ($file =~ m/\.png$/i) { return 'image/png' } |
|
51 |
elsif ($file =~ m/\.gif$/i) { return 'image/gif' } |
|
52 |
elsif ($file =~ m/\.jpe?g$/i) { return 'image/jpeg'} |
|
53 |
else { return 'application/octet-stream'} |
|
54 |
|
|
55 |
return; |
|
56 |
} |
|
57 | ||
58 |
sub blob_contenttype { |
|
59 |
my ($self, $fh, $file, $type) = @_; |
|
60 |
|
|
61 |
# Content type |
|
62 |
$type ||= $self->blob_mimetype($fh, $file); |
|
63 |
if ($type eq 'text/plain') { |
|
64 |
$type .= "; charset=" . $self->encoding; |
|
65 |
} |
|
66 | ||
67 |
return $type; |
|
68 |
} |
|
69 | ||
70 |
sub check_head_link { |
|
71 |
my ($self, $dir) = @_; |
|
72 |
|
|
73 |
# Chack head |
|
74 |
my $head_file = "$dir/HEAD"; |
|
75 |
return ((-e $head_file) || |
|
76 |
(-l $head_file && readlink($head_file) =~ /^refs\/heads\//)); |
|
77 |
} |
|
78 | ||
79 |
sub cmd { |
|
80 |
my ($self, $project) = @_; |
|
81 |
|
|
82 |
# Execute git command |
|
83 |
return ($self->bin, "--git-dir=$project"); |
|
84 |
} |
|
85 | ||
86 |
sub file_type { |
|
87 |
my ($self, $mode) = @_; |
|
88 |
|
|
89 |
# File type |
|
90 |
if ($mode !~ m/^[0-7]+$/) { return $mode } |
|
91 |
else { $mode = oct $mode } |
|
92 |
if ($self->_s_isgitlink($mode)) { return 'submodule' } |
|
93 |
elsif (S_ISDIR($mode & S_IFMT)) { return 'directory' } |
|
94 |
elsif (S_ISLNK($mode)) { return 'symlink' } |
|
95 |
elsif (S_ISREG($mode)) { return 'file' } |
|
96 |
else { return 'unknown' } |
|
97 |
|
|
98 |
return |
|
99 |
} |
|
100 | ||
101 |
sub file_type_long { |
|
102 |
my ($self, $mode) = @_; |
|
103 |
|
|
104 |
# File type |
|
105 |
if ($mode !~ m/^[0-7]+$/) { return $mode } |
|
106 |
else { $mode = oct $mode } |
|
107 |
if (S_ISGITLINK($mode)) { return 'submodule' } |
|
108 |
elsif (S_ISDIR($mode & S_IFMT)) { return 'directory' } |
|
109 |
elsif (S_ISLNK($mode)) { return 'symlink' } |
|
110 |
elsif (S_ISREG($mode)) { |
|
111 |
if ($mode & S_IXUSR) { return 'executable' } |
|
112 |
else { return 'file' } |
|
113 |
} |
|
114 |
else { return 'unknown' } |
|
115 |
|
|
116 |
return; |
|
117 |
} |
|
118 | ||
119 |
sub fill_from_file_info { |
|
120 |
my ($self, $project, $diff, $parents) = @_; |
|
121 |
|
|
122 |
# Fill file info |
|
123 |
$diff->{from_file} = []; |
|
124 |
$diff->{from_file}[$diff->{nparents} - 1] = undef; |
|
125 |
for (my $i = 0; $i < $diff->{nparents}; $i++) { |
|
126 |
if ($diff->{status}[$i] eq 'R' || $diff->{status}[$i] eq 'C') { |
|
127 |
$diff->{from_file}[$i] = |
|
128 |
$self->path_by_id($project, $parents->[$i], $diff->{from_id}[$i]); |
|
129 |
} |
|
130 |
} |
|
131 | ||
132 |
return $diff; |
|
133 |
} |
|
134 | ||
135 |
sub difftree { |
|
136 |
my ($self, $project, $cid, $parent, $parents) = @_; |
|
137 |
|
|
138 |
# Root |
|
139 |
$parent = '--root' unless defined $parent; |
|
140 | ||
141 |
# Command "git diff-tree" |
|
142 |
my @cmd = ($self->cmd($project), "diff-tree", '-r', '--no-commit-id', |
|
143 |
'-M', (@$parents <= 1 ? $parent : '-c'), $cid, '--'); |
|
144 |
open my $fh, "-|", @cmd |
|
145 |
or croak 500, "Open git-diff-tree failed"; |
|
146 |
my @difftree = map { chomp; $self->dec($_) } <$fh>; |
|
147 |
close $fh or croak 'Reading git-diff-tree failed'; |
|
148 |
|
|
149 |
# Parse "git diff-tree" output |
|
150 |
my $diffs = []; |
|
151 |
my @parents = @$parents; |
|
152 |
for my $line (@difftree) { |
|
153 |
my $diff = $self->parsed_difftree_line($line); |
|
154 |
|
|
155 |
# Parent are more than one |
|
156 |
if (exists $diff->{nparents}) { |
|
157 | ||
158 |
$self->fill_from_file_info($project, $diff, $parents) |
|
159 |
unless exists $diff->{from_file}; |
|
160 |
$diff->{is_deleted} = 1 if $self->is_deleted($diff); |
|
161 |
push @$diffs, $diff; |
|
162 |
} |
|
163 |
|
|
164 |
# Parent is single |
|
165 |
else { |
|
166 |
my ($to_mode_oct, $to_mode_str, $to_file_type); |
|
167 |
my ($from_mode_oct, $from_mode_str, $from_file_type); |
|
168 |
if ($diff->{to_mode} ne ('0' x 6)) { |
|
169 |
$to_mode_oct = oct $diff->{to_mode}; |
|
170 |
if (S_ISREG($to_mode_oct)) { # only for regular file |
|
171 |
$to_mode_str = sprintf('%04o', $to_mode_oct & 0777); # permission bits |
|
172 |
} |
|
173 |
$to_file_type = $self->file_type($diff->{to_mode}); |
|
174 |
} |
|
175 |
if ($diff->{from_mode} ne ('0' x 6)) { |
|
176 |
$from_mode_oct = oct $diff->{from_mode}; |
|
177 |
if (S_ISREG($from_mode_oct)) { # only for regular file |
|
178 |
$from_mode_str = sprintf('%04o', $from_mode_oct & 0777); # permission bits |
|
179 |
} |
|
180 |
$from_file_type = $self->file_type($diff->{from_mode}); |
|
181 |
} |
|
182 |
|
|
183 |
$diff->{to_mode_str} = $to_mode_str; |
|
184 |
$diff->{to_mode_oct} = $to_mode_oct; |
|
185 |
$diff->{to_file_type} = $to_file_type; |
|
186 |
$diff->{from_mode_str} = $from_mode_str; |
|
187 |
$diff->{from_mode_oct} = $from_mode_oct; |
|
188 |
$diff->{from_file_type} = $from_file_type; |
|
189 | ||
190 |
push @$diffs, $diff; |
|
191 |
} |
|
192 |
} |
|
193 |
|
|
194 |
return $diffs; |
|
195 |
} |
|
196 | ||
197 |
sub head_id { |
|
198 |
my ($self, $project) = (shift, shift); |
|
199 |
|
|
200 |
# HEAD id |
|
201 |
return $self->id($project, 'HEAD', @_); |
|
202 |
}; |
|
203 | ||
204 |
sub heads { |
|
205 |
my ($self, $project, $limit, @classes) = @_; |
|
206 |
|
|
207 |
# Command "git for-each-ref" (get heads) |
|
208 |
@classes = ('heads') unless @classes; |
|
209 |
my @patterns = map { "refs/$_" } @classes; |
|
210 |
my @cmd = ($self->cmd($project), 'for-each-ref', |
|
211 |
($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate', |
|
212 |
'--format=%(objectname) %(refname) %(subject)%00%(committer)', |
|
213 |
@patterns); |
|
214 |
open my $fh, '-|', @cmd or return; |
|
215 |
|
|
216 |
# Create head info |
|
217 |
my @heads; |
|
218 |
while (my $line = $self->dec(scalar <$fh>)) { |
|
219 |
my %ref_item; |
|
220 | ||
221 |
chomp $line; |
|
222 |
my ($refinfo, $committerinfo) = split(/\0/, $line); |
|
223 |
my ($cid, $name, $title) = split(' ', $refinfo, 3); |
|
224 |
my ($committer, $epoch, $tz) = |
|
225 |
($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); |
|
226 |
$ref_item{fullname} = $name; |
|
227 |
$name =~ s!^refs/(?:head|remote)s/!!; |
|
228 | ||
229 |
$ref_item{name} = $name; |
|
230 |
$ref_item{id} = $cid; |
|
231 |
$ref_item{title} = $title || '(no commit message)'; |
|
232 |
$ref_item{epoch} = $epoch; |
|
233 |
if ($epoch) { |
|
234 |
$ref_item{age} = $self->_age_string(time - $ref_item{epoch}); |
|
235 |
} else { $ref_item{age} = 'unknown' } |
|
236 | ||
237 |
push @heads, \%ref_item; |
|
238 |
} |
|
239 |
close $fh; |
|
240 | ||
241 |
return \@heads; |
|
242 |
} |
|
243 | ||
244 |
sub id { |
|
245 |
my ($self, $project, $ref, @options) = @_; |
|
246 |
|
|
247 |
# Command "git rev-parse" (get commit id) |
|
248 |
my $id; |
|
249 |
my @cmd = ($self->cmd($project), 'rev-parse', |
|
250 |
'--verify', '-q', @options, $ref); |
|
251 |
if (open my $fh, '-|', @cmd) { |
|
252 |
$id = $self->dec(scalar <$fh>); |
|
253 |
chomp $id if defined $id; |
|
254 |
close $fh; |
|
255 |
} |
|
256 |
|
|
257 |
return $id; |
|
258 |
} |
|
259 | ||
260 |
sub id_by_path { |
|
261 |
my ($self, $project, $commit_id, $path, $type) = @_; |
|
262 |
|
|
263 |
# Get blob id or tree id (command "git ls-tree") |
|
264 |
$path =~ s#/+$##; |
|
265 |
my @cmd = ($self->cmd($project), 'ls-tree', $commit_id, '--', $path); |
|
266 |
open my $fh, '-|', @cmd |
|
267 |
or croak 'Open git-ls-tree failed'; |
|
268 |
my $line = $self->dec(scalar <$fh>); |
|
269 |
close $fh or return; |
|
270 |
my ($t, $id) = ($line || '') =~ m/^[0-9]+ (.+) ([0-9a-fA-F]{40})\t/; |
|
271 |
return if defined $type && $type ne $t; |
|
272 | ||
273 |
return $id; |
|
274 |
} |
|
275 | ||
276 |
sub path_by_id { |
|
277 |
my $self = shift; |
|
278 |
my $project = shift; |
|
279 |
my $base = shift || return; |
|
280 |
my $hash = shift || return; |
|
281 |
|
|
282 |
# Command "git ls-tree" |
|
283 |
my @cmd = ($self->cmd($project), 'ls-tree', '-r', '-t', '-z', $base); |
|
284 |
open my $fh, '-|' or return; |
|
285 | ||
286 |
# Get path |
|
287 |
local $/ = "\0"; |
|
288 |
while (my $line = <$fh>) { |
|
289 |
$line = d$line; |
|
290 |
chomp $line; |
|
291 | ||
292 |
if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) { |
|
293 |
close $fh; |
|
294 |
return $1; |
|
295 |
} |
|
296 |
} |
|
297 |
close $fh; |
|
298 |
|
|
299 |
return; |
|
300 |
} |
|
301 | ||
302 |
sub project_description { |
|
303 |
my ($self, $project) = @_; |
|
304 |
|
|
305 |
# Description |
|
306 |
my $file = "$project/description"; |
|
307 |
my $description = $self->_slurp($file) || ''; |
|
308 |
|
|
309 |
return $description; |
|
310 |
} |
|
311 | ||
Fixed repositories page
|
312 |
sub repository_description { |
313 |
my ($self, $rep) = @_; |
|
314 |
|
|
315 |
# Description |
|
316 |
my $file = "$rep/description"; |
|
317 |
my $description = $self->_slurp($file) || ''; |
|
318 |
|
|
319 |
return $description; |
|
320 |
} |
|
321 | ||
copy gitweblite soruce code
|
322 |
sub last_activity { |
323 |
my ($self, $project) = @_; |
|
324 |
|
|
325 |
# Command "git for-each-ref" |
|
326 |
my @cmd = ($self->cmd($project), 'for-each-ref', |
|
327 |
'--format=%(committer)', '--sort=-committerdate', |
|
328 |
'--count=1', 'refs/heads'); |
|
329 |
open my $fh, '-|', @cmd or return; |
|
330 |
my $most_recent = $self->dec(scalar <$fh>); |
|
331 |
close $fh or return; |
|
332 |
|
|
333 |
# Parse most recent |
|
334 |
if (defined $most_recent && |
|
335 |
$most_recent =~ / (\d+) [-+][01]\d\d\d$/) { |
|
336 |
my $timestamp = $1; |
|
337 |
my $age = time - $timestamp; |
|
338 |
return ($age, $self->_age_string($age)); |
|
339 |
} |
|
340 |
|
|
341 |
return; |
|
342 |
} |
|
343 | ||
344 |
sub object_type { |
|
345 |
my ($self, $project, $cid) = @_; |
|
346 |
|
|
347 |
# Get object type (command "git cat-file") |
|
348 |
my @cmd = ($self->cmd($project), 'cat-file', '-t', $cid); |
|
349 |
open my $fh, '-|', @cmd or return; |
|
350 |
my $type = $self->dec(scalar <$fh>); |
|
351 |
close $fh or return; |
|
352 |
chomp $type; |
|
353 |
|
|
354 |
return $type; |
|
355 |
} |
|
356 | ||
357 |
sub project_owner { |
|
358 |
my ($self, $project) = @_; |
|
359 |
|
|
360 |
# Project owner |
|
361 |
my $user_id = (stat $project)[4]; |
|
362 |
my $user = getpwuid $user_id; |
|
363 |
|
|
364 |
return $user; |
|
365 |
} |
|
366 | ||
367 |
sub project_urls { |
|
368 |
my ($self, $project) = @_; |
|
369 |
|
|
370 |
# Project URLs |
|
371 |
open my $fh, '<', "$project/cloneurl" |
|
372 |
or return; |
|
373 |
my @urls = map { chomp; $self->dec($_) } <$fh>; |
|
374 |
close $fh; |
|
375 | ||
376 |
return \@urls; |
|
377 |
} |
|
378 | ||
379 |
sub references { |
|
380 |
my ($self, $project, $type) = @_; |
|
381 |
|
|
382 |
$type ||= ''; |
|
383 |
|
|
384 |
# Command "git show-ref" (get references) |
|
385 |
my @cmd = ($self->cmd($project), 'show-ref', '--dereference', |
|
386 |
($type ? ('--', "refs/$type") : ())); |
|
387 |
open my $fh, '-|', @cmd or return; |
|
388 |
|
|
389 |
# Parse references |
|
390 |
my %refs; |
|
391 |
while (my $line = $self->dec(scalar <$fh>)) { |
|
392 |
chomp $line; |
|
393 |
if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) { |
|
394 |
if (defined $refs{$1}) { push @{$refs{$1}}, $2 } |
|
395 |
else { $refs{$1} = [$2] } |
|
396 |
} |
|
397 |
} |
|
398 |
close $fh or return; |
|
399 |
|
|
400 |
return \%refs; |
|
401 |
} |
|
402 | ||
Fixed repositories page
|
403 |
sub fill_repositories { |
404 |
my ($self, $home, $ps) = @_; |
|
405 |
|
|
406 |
# Fill rep info |
|
407 |
my @resp; |
|
408 |
for my $rep (@$ps) { |
|
409 |
my (@activity) = $self->last_activity("$home/$rep->{path}"); |
|
410 |
next unless @activity; |
|
411 |
($rep->{age}, $rep->{age_string}) = @activity; |
|
412 |
if (!defined $rep->{descr}) { |
|
413 |
my $descr = $self->repository_description("$home/$rep->{path}") || ''; |
|
414 |
$rep->{descr_long} = $descr; |
|
415 |
$rep->{descr} = $self->_chop_str($descr, 25, 5); |
|
416 |
} |
|
417 | ||
418 |
push @resp, $rep; |
|
419 |
} |
|
420 | ||
421 |
return \@resp; |
|
422 |
} |
|
423 | ||
424 |
sub repositories { |
|
425 |
my ($self, $dir, %opt) = @_; |
|
426 |
|
|
427 |
my $filter = $opt{filter}; |
|
428 |
|
|
429 |
# Repositories |
|
430 |
opendir my $dh, $self->enc($dir) |
|
431 |
or croak qq/Can't open directory $dir: $!/; |
|
432 |
my @reps; |
|
433 |
while (my $rep = readdir $dh) { |
|
434 |
next unless $rep =~ /\.git$/; |
|
435 |
next unless $self->check_head_link("$dir/$rep"); |
|
436 |
next if defined $filter && $rep !~ /\Q$filter\E/; |
|
437 |
my $rep_name = $rep; |
|
438 |
$rep_name =~ s/\.git$//; |
|
439 |
push @reps, { name => $rep_name, path => $rep }; |
|
440 |
} |
|
441 | ||
442 |
# Fill repositroies information |
|
443 |
for my $rep (@reps) { |
|
444 |
my (@activity) = $self->last_activity("$dir/$rep->{path}"); |
|
445 |
next unless @activity; |
|
446 |
($rep->{age}, $rep->{age_string}) = @activity; |
|
447 |
if (!defined $rep->{descr}) { |
|
448 |
my $descr = $self->repository_description("$dir/$rep->{path}") || ''; |
|
449 |
$rep->{descr_long} = $descr; |
|
450 |
$rep->{descr} = $self->_chop_str($descr, 25, 5); |
|
451 |
} |
|
452 |
} |
|
453 | ||
454 |
return \@reps; |
|
455 |
} |
|
456 | ||
copy gitweblite soruce code
|
457 |
sub short_id { |
458 |
my ($self, $project) = (shift, shift); |
|
459 |
|
|
460 |
# Short id |
|
461 |
return $self->id($project, @_, '--short=7'); |
|
462 |
} |
|
463 | ||
464 |
sub tag { |
|
465 |
my ($self, $project, $name) = @_; |
|
466 |
|
|
467 |
# Tag |
|
468 |
my $tags = $self->tags($project); |
|
469 |
for my $tag (@$tags) { |
|
470 |
return $tag if $tag->{name} eq $name; |
|
471 |
} |
|
472 |
|
|
473 |
return; |
|
474 |
} |
|
475 | ||
476 |
sub tags { |
|
477 |
my ($self, $project, $limit) = @_; |
|
478 |
|
|
479 |
# Get tags (command "git for-each-ref") |
|
480 |
my @cmd = ($self->cmd($project), 'for-each-ref', |
|
481 |
($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate', |
|
482 |
'--format=%(objectname) %(objecttype) %(refname) '. |
|
483 |
'%(*objectname) %(*objecttype) %(subject)%00%(creator)', |
|
484 |
'refs/tags'); |
|
485 |
open my $fh, '-|', @cmd or return; |
|
486 |
|
|
487 |
# Parse Tags |
|
488 |
my @tags; |
|
489 |
while (my $line = $self->dec(scalar <$fh>)) { |
|
490 |
|
|
491 |
my %tag; |
|
492 | ||
493 |
chomp $line; |
|
494 |
my ($refinfo, $creatorinfo) = split(/\0/, $line); |
|
495 |
my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6); |
|
496 |
my ($creator, $epoch, $tz) = |
|
497 |
($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/); |
|
498 |
$tag{fullname} = $name; |
|
499 |
$name =~ s!^refs/tags/!!; |
|
500 | ||
501 |
$tag{type} = $type; |
|
502 |
$tag{id} = $id; |
|
503 |
$tag{name} = $name; |
|
504 |
if ($type eq 'tag') { |
|
505 |
$tag{subject} = $title; |
|
506 |
$tag{reftype} = $reftype; |
|
507 |
$tag{refid} = $refid; |
|
508 |
} else { |
|
509 |
$tag{reftype} = $type; |
|
510 |
$tag{refid} = $id; |
|
511 |
} |
|
512 | ||
513 |
if ($type eq 'tag' || $type eq 'commit') { |
|
514 |
$tag{epoch} = $epoch; |
|
515 |
if ($epoch) { |
|
516 |
$tag{age} = $self->_age_string(time - $tag{epoch}); |
|
517 |
} else { |
|
518 |
$tag{age} = 'unknown'; |
|
519 |
} |
|
520 |
} |
|
521 |
|
|
522 |
$tag{comment_short} = $self->_chop_str($tag{subject}, 30, 5) |
|
523 |
if $tag{subject}; |
|
524 | ||
525 |
push @tags, \%tag; |
|
526 |
} |
|
527 |
close $fh; |
|
528 | ||
529 |
return \@tags; |
|
530 |
} |
|
531 | ||
532 |
sub is_deleted { |
|
533 |
my ($self, $diffinfo) = @_; |
|
534 |
|
|
535 |
# Check if deleted |
|
536 |
return $diffinfo->{to_id} eq ('0' x 40); |
|
537 |
} |
|
538 | ||
539 |
sub id_set_multi { |
|
540 |
my ($self, $cid, $key, $value) = @_; |
|
541 | ||
542 |
if (!exists $cid->{$key}) { $cid->{$key} = $value } |
|
543 |
elsif (!ref $cid->{$key}) { $cid->{$key} = [ $cid->{$key}, $value ] } |
|
544 |
else { push @{$cid->{$key}}, $value } |
|
545 |
} |
|
546 | ||
547 |
sub parse_commit { |
|
548 |
my ($self, $project, $id) = @_; |
|
549 |
|
|
550 |
# Git rev-list |
|
551 |
my @cmd = ($self->cmd($project), 'rev-list', '--parents', |
|
552 |
'--header', '--max-count=1', $id, '--'); |
|
553 |
open my $fh, '-|', @cmd |
|
554 |
or croak 'Open git-rev-list failed'; |
|
555 |
|
|
556 |
# Parse commit |
|
557 |
local $/ = "\0"; |
|
558 |
my $content = $self->dec(scalar <$fh>); |
|
559 |
my $commit = $self->parse_commit_text($content, 1); |
|
560 |
close $fh; |
|
561 | ||
562 |
return $commit; |
|
563 |
} |
|
564 | ||
565 |
sub parse_commit_text { |
|
566 |
my ($self, $commit_text, $withparents) = @_; |
|
567 |
|
|
568 |
my @commit_lines = split '\n', $commit_text; |
|
569 |
my %commit; |
|
570 | ||
571 |
pop @commit_lines; # Remove '\0' |
|
572 |
return unless @commit_lines; |
|
573 | ||
574 |
my $header = shift @commit_lines; |
|
575 |
return if $header !~ m/^[0-9a-fA-F]{40}/; |
|
576 |
|
|
577 |
($commit{id}, my @parents) = split ' ', $header; |
|
578 |
while (my $line = shift @commit_lines) { |
|
579 |
last if $line eq "\n"; |
|
580 |
if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) { |
|
581 |
$commit{tree} = $1; |
|
582 |
} elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) { |
|
583 |
push @parents, $1; |
|
584 |
} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { |
|
585 |
$commit{author} = $1; |
|
586 |
$commit{author_epoch} = $2; |
|
587 |
$commit{author_tz} = $3; |
|
588 |
if ($commit{author} =~ m/^([^<]+) <([^>]*)>/) { |
|
589 |
$commit{author_name} = $1; |
|
590 |
$commit{author_email} = $2; |
|
591 |
} else { |
|
592 |
$commit{author_name} = $commit{author}; |
|
593 |
} |
|
594 |
} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) { |
|
595 |
$commit{committer} = $1; |
|
596 |
$commit{committer_epoch} = $2; |
|
597 |
$commit{committer_tz} = $3; |
|
598 |
if ($commit{committer} =~ m/^([^<]+) <([^>]*)>/) { |
|
599 |
$commit{committer_name} = $1; |
|
600 |
$commit{committer_email} = $2; |
|
601 |
} else { |
|
602 |
$commit{committer_name} = $commit{committer}; |
|
603 |
} |
|
604 |
} |
|
605 |
} |
|
606 |
return unless defined $commit{tree}; |
|
607 |
$commit{parents} = \@parents; |
|
608 |
$commit{parent} = $parents[0]; |
|
609 | ||
610 |
for my $title (@commit_lines) { |
|
611 |
$title =~ s/^ //; |
|
612 |
if ($title ne '') { |
|
613 |
$commit{title} = $self->_chop_str($title, 80, 5); |
|
614 |
# remove leading stuff of merges to make the interesting part visible |
|
615 |
if (length($title) > 50) { |
|
616 |
$title =~ s/^Automatic //; |
|
617 |
$title =~ s/^merge (of|with) /Merge ... /i; |
|
618 |
if (length($title) > 50) { |
|
619 |
$title =~ s/(http|rsync):\/\///; |
|
620 |
} |
|
621 |
if (length($title) > 50) { |
|
622 |
$title =~ s/(master|www|rsync)\.//; |
|
623 |
} |
|
624 |
if (length($title) > 50) { |
|
625 |
$title =~ s/kernel.org:?//; |
|
626 |
} |
|
627 |
if (length($title) > 50) { |
|
628 |
$title =~ s/\/pub\/scm//; |
|
629 |
} |
|
630 |
} |
|
631 |
$commit{title_short} = $self->_chop_str($title, 50, 5); |
|
632 |
last; |
|
633 |
} |
|
634 |
} |
|
635 |
if (! defined $commit{title} || $commit{title} eq '') { |
|
636 |
$commit{title} = $commit{title_short} = '(no commit message)'; |
|
637 |
} |
|
638 |
# remove added spaces |
|
639 |
for my $line (@commit_lines) { |
|
640 |
$line =~ s/^ //; |
|
641 |
} |
|
642 |
$commit{comment} = \@commit_lines; |
|
643 | ||
644 |
my $age = time - $commit{committer_epoch}; |
|
645 |
$commit{age} = $age; |
|
646 |
$commit{age_string} = $self->_age_string($age); |
|
647 |
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($commit{committer_epoch}); |
|
648 |
if ($age > 60*60*24*7*2) { |
|
649 |
$commit{age_string_date} = sprintf '%4i-%02u-%02i', 1900 + $year, $mon+1, $mday; |
|
650 |
$commit{age_string_age} = $commit{age_string}; |
|
651 |
} else { |
|
652 |
$commit{age_string_date} = $commit{age_string}; |
|
653 |
$commit{age_string_age} = sprintf '%4i-%02u-%02i', 1900 + $year, $mon+1, $mday; |
|
654 |
} |
|
655 |
return \%commit; |
|
656 |
} |
|
657 | ||
658 |
sub parse_commits { |
|
659 |
my ($self, $project, $cid, $maxcount, $skip, $file, @args) = @_; |
|
660 | ||
661 |
# git rev-list |
|
662 |
$maxcount ||= 1; |
|
663 |
$skip ||= 0; |
|
664 |
my @cmd = ($self->cmd($project), 'rev-list', '--header', @args, |
|
665 |
('--max-count=' . $maxcount), ('--skip=' . $skip), $cid, '--', |
|
666 |
($file ? ($file) : ())); |
|
667 |
open my $fh, '-|', @cmd |
|
668 |
or croak 'Open git-rev-list failed'; |
|
669 | ||
670 |
# Parse rev-list results |
|
671 |
local $/ = "\0"; |
|
672 |
my @commits; |
|
673 |
while (my $line = $self->dec(scalar <$fh>)) { |
|
674 |
my $commit = $self->parse_commit_text($line); |
|
675 |
push @commits, $commit; |
|
676 |
} |
|
677 |
close $fh; |
|
678 | ||
679 |
return \@commits; |
|
680 |
} |
|
681 | ||
682 |
sub parse_date { |
|
683 |
my $self = shift; |
|
684 |
my $epoch = shift; |
|
685 |
my $tz = shift || '-0000'; |
|
686 |
|
|
687 |
# Parse data |
|
688 |
my %date; |
|
689 |
my @months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/; |
|
690 |
my @days = qw/Sun Mon Tue Wed Thu Fri Sat/; |
|
691 |
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime $epoch; |
|
692 |
$date{hour} = $hour; |
|
693 |
$date{minute} = $min; |
|
694 |
$date{mday} = $mday; |
|
695 |
$date{day} = $days[$wday]; |
|
696 |
$date{month} = $months[$mon]; |
|
697 |
$date{rfc2822} = sprintf '%s, %d %s %4d %02d:%02d:%02d +0000', |
|
698 |
$days[$wday], $mday, $months[$mon], 1900 + $year, $hour ,$min, $sec; |
|
699 |
$date{'mday-time'} = sprintf '%d %s %02d:%02d', |
|
700 |
$mday, $months[$mon], $hour ,$min; |
|
701 |
$date{'iso-8601'} = sprintf '%04d-%02d-%02dT%02d:%02d:%02dZ', |
|
702 |
1900 + $year, 1+$mon, $mday, $hour ,$min, $sec; |
|
703 |
my ($tz_sign, $tz_hour, $tz_min) = ($tz =~ m/^([-+])(\d\d)(\d\d)$/); |
|
704 |
$tz_sign = ($tz_sign eq '-' ? -1 : +1); |
|
705 |
my $local = $epoch + $tz_sign * ((($tz_hour*60) + $tz_min) * 60); |
|
706 |
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime $local; |
|
707 |
$date{hour_local} = $hour; |
|
708 |
$date{minute_local} = $min; |
|
709 |
$date{tz_local} = $tz; |
|
710 |
$date{'iso-tz'} = sprintf('%04d-%02d-%02d %02d:%02d:%02d %s', |
|
711 |
1900 + $year, $mon+1, $mday, $hour, $min, $sec, $tz); |
|
712 |
|
|
713 |
return \%date; |
|
714 |
} |
|
715 | ||
716 |
sub parsed_difftree_line { |
|
717 |
my ($self, $line) = @_; |
|
718 |
|
|
719 |
return $line if ref $line eq 'HASH'; |
|
720 | ||
721 |
return $self->parse_difftree_raw_line($line); |
|
722 |
} |
|
723 | ||
724 |
sub parse_difftree_raw_line { |
|
725 |
my ($self, $line) = @_; |
|
726 | ||
727 |
my %res; |
|
728 |
if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) { |
|
729 |
$res{from_mode} = $1; |
|
730 |
$res{to_mode} = $2; |
|
731 |
$res{from_id} = $3; |
|
732 |
$res{to_id} = $4; |
|
733 |
$res{status} = $5; |
|
734 |
$res{similarity} = $6; |
|
735 |
if ($res{status} eq 'R' || $res{status} eq 'C') { |
|
736 |
($res{from_file}, $res{to_file}) = map { $self->_unquote($_) } split("\t", $7); |
|
737 |
} else { |
|
738 |
$res{from_file} = $res{to_file} = $res{file} = $self->_unquote($7); |
|
739 |
} |
|
740 |
} |
|
741 |
elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) { |
|
742 |
$res{nparents} = length($1); |
|
743 |
$res{from_mode} = [ split(' ', $2) ]; |
|
744 |
$res{to_mode} = pop @{$res{from_mode}}; |
|
745 |
$res{from_id} = [ split(' ', $3) ]; |
|
746 |
$res{to_id} = pop @{$res{from_id}}; |
|
747 |
$res{status} = [ split('', $4) ]; |
|
748 |
$res{to_file} = $self->_unquote($5); |
|
749 |
} |
|
750 |
elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { $res{commit} = $1 } |
|
751 | ||
752 |
return \%res; |
|
753 |
} |
|
754 | ||
755 |
sub parse_ls_tree_line { |
|
756 |
my ($self, $line) = @_; |
|
757 |
my %opts = @_; |
|
758 |
my %res; |
|
759 | ||
760 |
if ($opts{'-l'}) { |
|
761 |
$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s; |
|
762 | ||
763 |
$res{mode} = $1; |
|
764 |
$res{type} = $2; |
|
765 |
$res{hash} = $3; |
|
766 |
$res{size} = $4; |
|
767 |
if ($opts{'-z'}) { $res{name} = $5 } |
|
768 |
else { $res{name} = $self->_unquote($5) } |
|
769 |
} |
|
770 |
else { |
|
771 |
$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s; |
|
772 | ||
773 |
$res{mode} = $1; |
|
774 |
$res{type} = $2; |
|
775 |
$res{hash} = $3; |
|
776 |
if ($opts{'-z'}) { $res{name} = $4 } |
|
777 |
else { $res{name} = $self->_unquote($4) } |
|
778 |
} |
|
779 | ||
780 |
return \%res; |
|
781 |
} |
|
782 | ||
783 |
sub parse_tag { |
|
784 |
my ($self, $project, $tag_id) = @_; |
|
785 |
|
|
786 |
# Get tag (command "git cat-file") |
|
787 |
my @cmd = ($self->cmd($project), 'cat-file', 'tag', $tag_id); |
|
788 |
open my $fh, '-|', @cmd or return; |
|
789 |
|
|
790 |
# Parse tag |
|
791 |
my %tag; |
|
792 |
my @comment; |
|
793 |
$tag{id} = $tag_id; |
|
794 |
while (my $line = $self->dec(scalar <$fh>)) { |
|
795 |
chomp $line; |
|
796 |
if ($line =~ m/^object ([0-9a-fA-F]{40})$/) { $tag{object} = $1 } |
|
797 |
elsif ($line =~ m/^type (.+)$/) { $tag{type} = $1 } |
|
798 |
elsif ($line =~ m/^tag (.+)$/) { $tag{name} = $1 } |
|
799 |
elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) { |
|
800 |
$tag{author} = $1; |
|
801 |
$tag{author_epoch} = $2; |
|
802 |
$tag{author_tz} = $3; |
|
803 |
if ($tag{author} =~ m/^([^<]+) <([^>]*)>/) { |
|
804 |
$tag{author_name} = $1; |
|
805 |
$tag{author_email} = $2; |
|
806 |
} else { $tag{author_name} = $tag{author} } |
|
807 |
} elsif ($line =~ m/--BEGIN/) { |
|
808 |
push @comment, $line; |
|
809 |
last; |
|
810 |
} elsif ($line eq '') { last } |
|
811 |
} |
|
812 |
my $comment = $self->dec(scalar <$fh>); |
|
813 |
push @comment, $comment; |
|
814 |
$tag{comment} = \@comment; |
|
815 |
close $fh or return; |
|
816 |
return unless defined $tag{name}; |
|
817 |
|
|
818 |
return \%tag; |
|
819 |
} |
|
820 | ||
821 |
sub search_bin { |
|
822 |
my $self = shift; |
|
823 |
|
|
824 |
# Search git bin |
|
825 |
my $env_path = $ENV{PATH}; |
|
826 |
my @paths = split /:/, $env_path; |
|
827 |
for my $path (@paths) { |
|
828 |
$path =~ s#/$##; |
|
829 |
my $bin = "$path/git"; |
|
830 |
if (-f $bin) { |
|
831 |
return $bin; |
|
832 |
last; |
|
833 |
} |
|
834 |
} |
|
improved git path searching
|
835 |
|
836 |
my $local_bin = '/usr/local/bin/git'; |
|
837 |
return $local_bin if -f $local_bin; |
|
838 |
|
|
839 |
my $bin = '/usr/bin/git'; |
|
840 |
return $bin if -f $bin; |
|
841 |
|
|
copy gitweblite soruce code
|
842 |
return; |
843 |
} |
|
844 | ||
845 |
sub search_projects { |
|
846 |
my ($self, %opt) = @_; |
|
847 |
my $dirs = $self->search_dirs; |
|
848 |
my $max_depth = $self->search_max_depth; |
|
849 |
|
|
850 |
# Search |
|
851 |
my @projects; |
|
852 |
for my $dir (@$dirs) { |
|
853 |
next unless -d $dir; |
|
854 |
|
|
855 |
$dir =~ s/\/$//; |
|
856 |
my $prefix_length = length($dir); |
|
857 |
my $prefix_depth = 0; |
|
858 |
for my $c (split //, $dir) { |
|
859 |
$prefix_depth++ if $c eq '/'; |
|
860 |
} |
|
861 |
|
|
862 |
no warnings 'File::Find'; |
|
863 |
File::Find::find({ |
|
864 |
follow_fast => 1, |
|
865 |
follow_skip => 2, |
|
866 |
dangling_symlinks => 0, |
|
867 |
wanted => sub { |
|
868 |
my $path = $File::Find::name; |
|
869 |
my $base_path = $_; |
|
870 |
|
|
871 |
return if (m!^[/.]$!); |
|
872 |
return unless -d $base_path; |
|
873 |
|
|
874 |
if ($base_path eq '.git') { |
|
875 |
$File::Find::prune = 1; |
|
876 |
return; |
|
877 |
}; |
|
878 |
|
|
879 |
my $depth = 0; |
|
880 |
for my $c (split //, $dir) { |
|
881 |
$depth++ if $c eq '/'; |
|
882 |
} |
|
883 |
|
|
884 |
if ($depth - $prefix_depth > $max_depth) { |
|
885 |
$File::Find::prune = 1; |
|
886 |
return; |
|
887 |
} |
|
888 |
|
|
889 |
if (-d $path) { |
|
890 |
if ($self->check_head_link($path)) { |
|
891 |
my $home = dirname $path; |
|
892 |
my $name = basename $path; |
|
893 |
push @projects, {home => $home, name => $name}; |
|
894 |
$File::Find::prune = 1; |
|
895 |
} |
|
896 |
} |
|
897 |
}, |
|
898 |
}, $dir); |
|
899 |
} |
|
900 |
|
|
901 |
return \@projects; |
|
902 |
} |
|
903 | ||
904 |
sub snapshot_name { |
|
905 |
my ($self, $project, $cid) = @_; |
|
906 | ||
907 |
my $name = $project; |
|
908 |
$name =~ s,([^/])/*\.git$,$1,; |
|
909 |
$name = basename($name); |
|
910 |
# sanitize name |
|
911 |
$name =~ s/[[:cntrl:]]/?/g; |
|
912 | ||
913 |
my $ver = $cid; |
|
914 |
if ($cid =~ /^[0-9a-fA-F]+$/) { |
|
915 |
my $full_hash = $self->id($project, $cid); |
|
916 |
if ($full_hash =~ /^$cid/ && length($cid) > 7) { |
|
917 |
$ver = $self->short_id($project, $cid); |
|
918 |
} |
|
919 |
} elsif ($cid =~ m!^refs/tags/(.*)$!) { |
|
920 |
$ver = $1; |
|
921 |
} else { |
|
922 |
if ($cid =~ m!^refs/(?:heads|remotes)/(.*)$!) { |
|
923 |
$ver = $1; |
|
924 |
} |
|
925 |
$ver .= '-' . $self->short_id($project, $cid); |
|
926 |
} |
|
927 |
$ver =~ s!/!.!g; |
|
928 | ||
929 |
$name = "$name-$ver"; |
|
930 | ||
931 |
return wantarray ? ($name, $name) : $name; |
|
932 |
} |
|
933 | ||
934 |
sub _age_string { |
|
935 |
my ($self, $age) = @_; |
|
936 |
my $age_str; |
|
937 | ||
938 |
if ($age > 60*60*24*365*2) { |
|
939 |
$age_str = (int $age/60/60/24/365); |
|
940 |
$age_str .= ' years ago'; |
|
941 |
} elsif ($age > 60*60*24*(365/12)*2) { |
|
942 |
$age_str = int $age/60/60/24/(365/12); |
|
943 |
$age_str .= ' months ago'; |
|
944 |
} elsif ($age > 60*60*24*7*2) { |
|
945 |
$age_str = int $age/60/60/24/7; |
|
946 |
$age_str .= ' weeks ago'; |
|
947 |
} elsif ($age > 60*60*24*2) { |
|
948 |
$age_str = int $age/60/60/24; |
|
949 |
$age_str .= ' days ago'; |
|
950 |
} elsif ($age > 60*60*2) { |
|
951 |
$age_str = int $age/60/60; |
|
952 |
$age_str .= ' hours ago'; |
|
953 |
} elsif ($age > 60*2) { |
|
954 |
$age_str = int $age/60; |
|
955 |
$age_str .= ' min ago'; |
|
956 |
} elsif ($age > 2) { |
|
957 |
$age_str = int $age; |
|
958 |
$age_str .= ' sec ago'; |
|
959 |
} else { |
|
960 |
$age_str .= ' right now'; |
|
961 |
} |
|
962 |
return $age_str; |
|
963 |
} |
|
964 | ||
965 |
sub _chop_str { |
|
966 |
my $self = shift; |
|
967 |
my $str = shift; |
|
968 |
my $len = shift; |
|
969 |
my $add_len = shift || 10; |
|
970 |
my $where = shift || 'right'; |
|
971 | ||
972 |
if ($where eq 'center') { |
|
973 |
# Filler is length 5 |
|
974 |
return $str if ($len + 5 >= length($str)); |
|
975 |
$len = int($len/2); |
|
976 |
} else { |
|
977 |
# Filler is length 4 |
|
978 |
return $str if ($len + 4 >= length($str)); |
|
979 |
} |
|
980 | ||
981 |
# Regexps: ending and beginning with word part up to $add_len |
|
982 |
my $endre = qr/.{$len}\w{0,$add_len}/; |
|
983 |
my $begre = qr/\w{0,$add_len}.{$len}/; |
|
984 | ||
985 |
if ($where eq 'left') { |
|
986 |
$str =~ m/^(.*?)($begre)$/; |
|
987 |
my ($lead, $body) = ($1, $2); |
|
988 |
if (length($lead) > 4) { |
|
989 |
$lead = ' ...'; |
|
990 |
} |
|
991 |
return "$lead$body"; |
|
992 | ||
993 |
} elsif ($where eq 'center') { |
|
994 |
$str =~ m/^($endre)(.*)$/; |
|
995 |
my ($left, $str) = ($1, $2); |
|
996 |
$str =~ m/^(.*?)($begre)$/; |
|
997 |
my ($mid, $right) = ($1, $2); |
|
998 |
if (length($mid) > 5) { |
|
999 |
$mid = ' ... '; |
|
1000 |
} |
|
1001 |
return "$left$mid$right"; |
|
1002 | ||
1003 |
} else { |
|
1004 |
$str =~ m/^($endre)(.*)$/; |
|
1005 |
my $body = $1; |
|
1006 |
my $tail = $2; |
|
1007 |
if (length($tail) > 4) { |
|
1008 |
$tail = '... '; |
|
1009 |
} |
|
1010 |
return "$body$tail"; |
|
1011 |
} |
|
1012 |
} |
|
1013 | ||
1014 |
sub _mode_str { |
|
1015 |
my $self = shift; |
|
1016 |
my $mode = oct shift; |
|
1017 | ||
1018 |
# Mode to string |
|
1019 |
if ($self->_s_isgitlink($mode)) { return 'm---------' } |
|
1020 |
elsif (S_ISDIR($mode & S_IFMT)) { return 'drwxr-xr-x' } |
|
1021 |
elsif (S_ISLNK($mode)) { return 'lrwxrwxrwx' } |
|
1022 |
elsif (S_ISREG($mode)) { |
|
1023 |
if ($mode & S_IXUSR) { |
|
1024 |
return '-rwxr-xr-x'; |
|
1025 |
} else { |
|
1026 |
return '-rw-r--r--' |
|
1027 |
} |
|
1028 |
} else { return '----------' } |
|
1029 |
|
|
1030 |
return; |
|
1031 |
} |
|
1032 | ||
1033 |
sub _s_isgitlink { |
|
1034 |
my ($self, $mode) = @_; |
|
1035 |
|
|
1036 |
# Check if git link |
|
1037 |
my $s_ifgitlink = 0160000; |
|
1038 |
return (($mode & S_IFMT) == $s_ifgitlink) |
|
1039 |
} |
|
1040 | ||
1041 |
sub timestamp { |
|
1042 |
my ($self, $date) = @_; |
|
1043 |
|
|
1044 |
# Time stamp |
|
1045 |
my $strtime = $date->{rfc2822}; |
|
1046 |
my $localtime_format = '(%02d:%02d %s)'; |
|
1047 |
if ($date->{hour_local} < 6) { $localtime_format = '(%02d:%02d %s)' } |
|
1048 |
$strtime .= ' ' . sprintf( |
|
1049 |
$localtime_format, |
|
1050 |
$date->{hour_local}, |
|
1051 |
$date->{minute_local}, |
|
1052 |
$date->{tz_local} |
|
1053 |
); |
|
1054 | ||
1055 |
return $strtime; |
|
1056 |
} |
|
1057 | ||
1058 |
sub _slurp { |
|
1059 |
my ($self, $file) = @_; |
|
1060 |
|
|
1061 |
# Slurp |
|
1062 |
open my $fh, '<', $file |
|
1063 |
or croak qq/Can't open file "$file": $!/; |
|
1064 |
my $content = do { local $/; $self->dec(scalar <$fh>) }; |
|
1065 |
close $fh; |
|
1066 |
|
|
1067 |
return $content; |
|
1068 |
} |
|
1069 | ||
1070 |
sub _unquote { |
|
1071 |
my ($self, $str) = @_; |
|
1072 |
|
|
1073 |
# Unquote function |
|
1074 |
my $unq = sub { |
|
1075 |
my $seq = shift; |
|
1076 |
my %escapes = ( |
|
1077 |
t => "\t", |
|
1078 |
n => "\n", |
|
1079 |
r => "\r", |
|
1080 |
f => "\f", |
|
1081 |
b => "\b", |
|
1082 |
a => "\a", |
|
1083 |
e => "\e", |
|
1084 |
v => "\013", |
|
1085 |
); |
|
1086 | ||
1087 |
if ($seq =~ m/^[0-7]{1,3}$/) { return chr oct $seq } |
|
1088 |
elsif (exists $escapes{$seq}) { return $escapes{$seq} } |
|
1089 |
|
|
1090 |
return $seq; |
|
1091 |
}; |
|
1092 |
|
|
1093 |
# Unquote |
|
1094 |
if ($str =~ m/^"(.*)"$/) { |
|
1095 |
$str = $1; |
|
1096 |
$str =~ s/\\([^0-7]|[0-7]{1,3})/$unq->($1)/eg; |
|
1097 |
} |
|
1098 |
|
|
1099 |
return $str; |
|
1100 |
} |
|
1101 | ||
1102 |
sub _tab_to_space { |
|
1103 |
my ($self, $line) = @_; |
|
1104 |
|
|
1105 |
# Tab to space |
|
1106 |
while ((my $pos = index($line, "\t")) != -1) { |
|
1107 |
if (my $count = (2 - ($pos % 2))) { |
|
1108 |
my $spaces = ' ' x $count; |
|
1109 |
$line =~ s/\t/$spaces/; |
|
1110 |
} |
|
1111 |
} |
|
1112 | ||
1113 |
return $line; |
|
1114 |
} |
|
1115 | ||
1116 |
1; |