Newer Older
1116 lines | 27.521kb
renamed gitpub to gitprep
Yuki Kimoto authored on 2012-11-26
1
package Gitprep::Git;
copy gitweblite soruce code
root authored on 2012-11-23
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
Yuki Kimoto authored on 2012-11-23
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
root authored on 2012-11-23
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
Yuki Kimoto authored on 2012-11-23
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
root authored on 2012-11-23
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
Yuki Kimoto authored on 2012-11-23
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
root authored on 2012-11-23
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;