2ad0cd3 11 years ago
1 contributor
783 lines | 19.004kb
package Gitprep::Main;
use Mojo::Base 'Mojolicious::Controller';
use File::Basename 'dirname';
use Carp 'croak';

sub blob {
  my $self = shift;

  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id_file = $self->param('id_file');

  # Id and file
  my ($id, $file) = $self->_parse_id_path($project, $id_file);

  # Git
  my $git = $self->app->git;

  # Blob content
  my $bid = $git->id_by_path($project, $id, $file, 'blob')
    or croak 'Cannot find file';
  my @cmd = ($git->cmd($project), 'cat-file', 'blob', $bid);
  open my $fh, '-|', @cmd
    or croak qq/Couldn't cat "$file", "$bid"/;
  
  # Blob plain
  if ($self->stash('plain')) {
    # Content type
    my $type = $git->blob_contenttype($fh, $file);

    # Convert text/* content type to text/plain
    if ($self->config('prevent_xss') &&
      ($type =~ m#^text/[a-z]+\b(.*)$# ||
      ($type =~ m#^[a-z]+/[a-z]\+xml\b(.*)$# && -T $fh)))
    {
      my $rest = $1;
      $rest = defined $rest ? $rest : '';
      $type = "text/plain$rest";
    }

    # File name
    my $file_name = $id;
    if (defined $file) { $file_name = $file }
    elsif ($type =~ m/^text\//) { $file_name .= '.txt' }
    
    # Content
    my $content = do { local $/; <$fh> };
    my $sandbox = $self->config('prevent_xss') &&
      $type !~ m#^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)#;
    my $content_disposition = $sandbox ? 'attachment' : 'inline';
    $content_disposition .= "; filename=$file_name";
    
    # Render
    $self->res->headers->content_disposition($content_disposition);
    $self->res->headers->content_type($type);
    $self->render_data($content);
  }
  
  # Blob
  else {
    # MIME type
    my $mimetype = $git->blob_mimetype($fh, $file);
    
    # Redirect to blob-plain if no display MIME type
    if ($mimetype !~ m#^(?:text/|image/(?:gif|png|jpeg)$)# && -B $fh) {
      close $fh;
      my $url = $self->url_for('blob_plain',
        project => $project_ns, id_file => "$id/$file");
      
      return $self->redirect_to($url);
    }
    
    # Commit
    my $commit = $git->parse_commit($project, $id);

    # Parse line
    my @lines;
    while (my $line = $git->dec(scalar <$fh>)) {
      chomp $line;
      $line = $git->_tab_to_space($line);
      push @lines, $line;
    }
    
    # Render
    $self->render(
      home => $home,
      home_ns => $home_ns,
      project => $project,
      project_ns => $project_ns,
      commit => $commit,
      id => $id,
      file => $file,
      bid => $bid,
      lines => \@lines,
      mimetype => $mimetype
    );
  }
}

sub blobdiff {
  my $self = shift;

  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $diff = $self->param('diff');
  my $file = $self->param('file');
  my $from_file = $self->param('from-file');
  $from_file = $file unless defined $from_file;
  my $plain = $self->param('plain');
  my $from_id;
  my $id;
  if ($diff =~ /\.\./) { ($from_id, $id) = $diff =~ /(.+)\.\.(.+)/ }
  else { $id = $diff }
  
  # Git
  my $git = $self->app->git;

  # Get blob diff (command "git diff")
  open my $fh, '-|', $git->cmd($project), 'diff', '-r', '-M', '-p',
      $from_id, $id, '--', $from_file, $file
    or croak "Open git-diff-tree failed";
  
  # Blob diff plain
  if ($plain) {
    # Content
    my $content = do { local $/; <$fh> };
    close $fh;
    
    # Render
    my $content_disposition .= "inline; filename=$file";
    $self->res->headers->content_disposition($content_disposition);
    $self->res->headers->content_type("text/plain; charset=" . $git->encoding);
    $self->render(data => $content);
  }
  
  # Blob diff
  else {
    # Lines
    my @lines = map { $git->dec($_) } <$fh>;
    close $fh;
    my $lines = $self->_parse_blobdiff_lines(\@lines);
    
    # Commit
    my $commit = $git->parse_commit($project, $id);
    
    # Render
    $self->render(
      '/blobdiff',
      home => $home,
      home_ns => $home_ns,
      project => $project,
      project_ns => $project_ns,
      id => $id,
      from_id => $from_id,
      file => $file,
      from_file => $from_file,
      commit => $commit,
      lines => $lines
    );
  }
}

sub commit {
  my $self = shift;

  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id = $self->param('id');
  
  # Git
  my $git = $self->app->git;

  # Commit
  my $commit = $git->parse_commit($project, $id);
  my $committer_date
    = $git->parse_date($commit->{committer_epoch}, $commit->{committer_tz});
  my $author_date
    = $git->parse_date($commit->{author_epoch}, $commit->{author_tz});
  $commit->{author_date} = $git->timestamp($author_date);
  $commit->{committer_date} = $git->timestamp($committer_date);
  
  # References
  my $refs = $git->references($project);
  
  # Diff tree
  my $parent = $commit->{parent};
  my $parents = $commit->{parents};
  my $difftrees = $git->difftree($project, $commit->{id}, $parent, $parents);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    id => $id,
    commit => $commit,
    refs => $refs,
    difftrees => $difftrees,
  );
}

sub commitdiff {
  my $self = shift;
  
  # Paramters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $diff = $self->param('diff');
  my ($from_id, $id) = $diff =~ /(.+)\.\.(.+)/;
  $id = $diff unless defined $id;
  
  # Git
  my $git = $self->app->git;
  
  # Commit
  my $commit = $git->parse_commit($project, $id)
    or croak 'Unknown commit object';
  my $author_date
    = $git->parse_date($commit->{author_epoch}, $commit->{author_tz});
  my $committer_date
    = $git->parse_date($commit->{committer_epoch}, $commit->{committer_tz});
  $commit->{author_date} = $git->timestamp($author_date);
  $commit->{committer_date} = $git->timestamp($committer_date);
  $from_id = $commit->{parent} unless defined $from_id;
  
  # Plain text
  if ($self->param('plain')) {
    # Get blob diffs (command "git diff-tree")
    my @cmd = ($git->cmd($project), 'diff-tree', '-r', '-M',
        '-p', $from_id, $id, '--');
    open my $fh, '-|', @cmd
      or croak 'Open git-diff-tree failed';

    # Content
    my $content = do { local $/; <$fh> };
    my $content_disposition .= "inline; filename=$id";
    
    # Render
    $self->res->headers->content_disposition($content_disposition);
    $self->res->headers->content_type("text/plain;charset=" . $git->encoding);
    $self->render_data($content);
  }
  
  # HTML
  else {
    
    # Diff tree
    my $difftrees = $git->difftree($project,
      $id, $commit->{parent}, $commit->{parents});
    
    # Get blob diffs (command "git diff-tree")
    my @cmd = ($git->cmd($project), 'diff-tree', '-r', '-M',
      '--no-commit-id', '--patch-with-raw', $from_id, $id, '--');
    open my $fh, '-|', @cmd
      or croak 'Open git-diff-tree failed';

    # Parse output
    my @blobdiffs;
    while (my $line = $git->dec(scalar <$fh>)) {
      
      # Parse line
      chomp $line;
      my $diffinfo = $git->parse_difftree_raw_line($line);
      my $from_file = $diffinfo->{from_file};
      my $file = $diffinfo->{to_file};
      
      # Get blobdiff (command "git diff-tree")
      my @cmd = ($git->cmd($project), 'diff-tree', '-r', '-M', '-p',
        $from_id, $id, '--', (defined $from_file ? $from_file : ()), $file);
      open my $fh_blobdiff, '-|', @cmd
        or croak 'Open git-diff-tree failed';
      my @lines = map { $git->dec($_) } <$fh>;
      close $fh_blobdiff;
      my $blobdiff = {
        file => $file,
        from_file => $from_file,
        lines => $self->_parse_blobdiff_lines(\@lines)
      };
      
      # Status
      for my $difftree (@$difftrees) {
        if ($difftree->{to_file} eq $file) {
          $blobdiff->{status} = $difftree->{status};
          last;
        }
      }
      
      push @blobdiffs, $blobdiff;
    }

    # References
    my $refs = $git->references($project);
    
    # Render
    $self->render(
      'commitdiff',
      home => $home,
      home_ns => $home_ns,
      project => $project,
      project_ns => $project_ns,
      from_id => $from_id,
      id => $id,
      commit => $commit,
      difftrees => $difftrees,
      blobdiffs => \@blobdiffs,
      refs => $refs
    );
  }
}

sub home {
  my $self = shift;

  my $rep_home = '/gitpub';
  my @users = qw/kimoto ken/;

  $self->render(users => \@users);
}

sub heads {
  my $self = shift;
  
  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  
  # Git
  my $git = $self->app->git;
  
  # Ref names
  my $heads  = $git->heads($project);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    heads => $heads,
  );
}

sub log {
  my ($self, %opt) = @_;

  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id = $self->param('id');
  my $page = $self->param('page');
  $page = 0 if !defined $page;
  my $short = $self->param('short');
  
  # Git
  my $git = $self->app->git;
  
  # Commit
  my $commit = $git->parse_commit($project, $id);
  
  # Commits
  my $page_count = $short ? 50 : 20;
  my $commits = $git->parse_commits(
    $project, $commit->{id},$page_count, $page_count * $page);
  for my $commit (@$commits) {
    my $author_date
      = $git->parse_date($commit->{author_epoch}, $commit->{author_tz});
    $commit->{author_date} = $git->timestamp($author_date);
  }
  
  # References
  my $refs = $git->references($project);

  # Render
  $self->stash->{action} = 'shortlog' if $short;
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    id => $id,
    commits => $commits,
    refs => $refs,
    page => $page,
    page_count => $page_count
  );
};

has 'root' => '/gitpub';

sub _root_ns {
  my $self = shift;
  
  my $root = $self->root;
  $root =~ s/^\///;
  
  return $root;
}

sub repository {
  my $self = shift;
  
  # Parameters
  my $user = $self->param('user');
  my $repository = $self->param('repository');
  my $root_ns = $self->_root_ns;
  
  my $project_ns = "$root_ns/$user/$repository.git";
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id_dir = $self->param('id_dir') || 'master/';

  # Id and directory
  my ($id, $dir) = $self->_parse_id_path($project, $id_dir);

  # Git
  my $git = $self->app->git;
  
  # Tree id
  my $tid;
  my $commit = $git->parse_commit($project, $id);
  unless (defined $tid) {
    if (defined $dir && $dir ne '') {
      $tid = $git->id_by_path($project, $id, $dir, 'tree');
    }
    else { $tid = $commit->{tree} }
  }
  $self->render_not_found unless defined $tid;

  # Get tree (command "git ls-tree")
  my @entries = ();
  my $show_sizes = 0;
  open my $fh, '-|', $git->cmd($project), 'ls-tree', '-z',
      ($show_sizes ? '-l' : ()), $tid
    or croak 'Open git-ls-tree failed';
  local $/ = "\0";
  @entries = map { chomp; $git->dec($_) } <$fh>;
  close $fh
    or croak 404, "Reading tree failed";
  
  # Parse tree
  my @trees;
  for my $line (@entries) {
    my $tree = $git->parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
    $tree->{mode_str} = $git->_mode_str($tree->{mode});
    push @trees, $tree;
  }
  
  # References
  my $refs = $git->references($project);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    dir => $dir,
    id => $id,
    tid => $tid,
    commit => $commit,
    trees => \@trees,
    refs => $refs
  );
}

sub repositories {
  my $self = shift;

  my $root = $self->root;
  my $user = $self->param('user');
  
  # Repositories
  my $reps = $self->app->git->repositories("/$root/$user");
  
  # Render
  $self->render(reps => $reps);
}

sub snapshot {
  my $self = shift;

  # Parameter
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id = $self->param('id');
  
  # Git
  my $git = $self->app->git;

  # Object type
  my $type = $git->object_type($project, "$id^{}");
  if (!$type) { croak 404, 'Object does not exist' }
  elsif ($type eq 'blob') { croak 400, 'Object is not a tree-ish' }
  
  my ($name, $prefix) = $git->snapshot_name($project, $id);
  my $file = "$name.tar.gz";
  my $cmd = $self->_quote_command(
    $git->cmd($project), 'archive', "--format=tar", "--prefix=$prefix/", $id
  );
  $cmd .= ' | ' . $self->_quote_command('gzip', '-n');

  $file =~ s/(["\\])/\\$1/g;

  open my $fh, '-|', $cmd
    or croak 'Execute git-archive failed';
  
  # Write chunk
  $self->res->headers->content_type('application/x-tar');
  $self->res->headers->content_disposition(qq/attachment; filename="$file"/);
  my $cb;
  $cb = sub {
    my $c = shift;
    my $size = 500 * 1024;
    my $length = sysread($fh, my $buffer, $size);
    unless (defined $length) {
      close $fh;
      undef $cb;
      return;
    }
    $c->write_chunk($buffer, $cb);
  };
  $self->$cb;
}

sub summary {
  my $self = shift;
  
  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  
  # Git
  my $git = $self->app->git;
  
  # HEAd commit
  my $project_description = $git->project_description($project);
  my $project_owner = $git->project_owner($project);
  my $head_commit = $git->parse_commit($project, 'HEAD');
  my $committer_date
    = $git->parse_date($head_commit->{committer_epoch}, $head_commit->{committer_tz});
  my $last_change = $git->timestamp($committer_date);
  my $head_id = $head_commit->{id};
  my $urls = $git->project_urls($project);
  
  # Commits
  my $commit_count = 20;
  my $commits = $head_id ? $git->parse_commits($project, $head_id, $commit_count) : ();

  # References
  my $refs = $git->references($project);
  
  # Tags
  my $tag_count = 20;
  my $tags  = $git->tags($project, $tag_count - 1);

  # Heads
  my $head_count = 20;
  my $heads = $git->heads($project, $head_count - 1);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    project_description => $project_description,
    project_owner => $project_owner,
    last_change => $last_change,
    urls => $urls,
    commits => $commits,
    tags => $tags,
    head_id => $head_id,
    heads => $heads,
    refs => $refs,
    commit_count => $commit_count,
    tag_count => $tag_count,
    head_count => $head_count
  );
}

sub tag {
  my $self = shift;
  
  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id = $self->param('id');
  
  # Git
  my $git = $self->app->git;
  
  # Ref names
  my $tag  = $git->parse_tag($project, $id);
  my $author_date
    = $git->parse_date($tag->{author_epoch}, $tag->{author_tz});
  $tag->{author_date} = $git->timestamp($author_date);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    tag => $tag,
  );
}

sub tags {
  my $self = shift;
  
  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  
  # Git
  my $git = $self->app->git;
  
  # Ref names
  my $tags  = $git->tags($project);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    tags => $tags,
  );
}

sub tree {
  my $self = shift;
  
  # Parameters
  my $project_ns = $self->param('project');
  my $project = "/$project_ns";
  my $home_ns = dirname $project_ns;
  my $home = "/$home_ns";
  my $id_dir = $self->param('id_dir');

  # Id and directory
  my ($id, $dir) = $self->_parse_id_path($project, $id_dir);

  # Git
  my $git = $self->app->git;
  
  # Tree id
  my $tid;
  my $commit = $git->parse_commit($project, $id);
  unless (defined $tid) {
    if (defined $dir && $dir ne '') {
      $tid = $git->id_by_path($project, $id, $dir, 'tree');
    }
    else { $tid = $commit->{tree} }
  }
  $self->render_not_found unless defined $tid;

  # Get tree (command "git ls-tree")
  my @entries = ();
  my $show_sizes = 0;
  open my $fh, '-|', $git->cmd($project), 'ls-tree', '-z',
      ($show_sizes ? '-l' : ()), $tid
    or croak 'Open git-ls-tree failed';
  local $/ = "\0";
  @entries = map { chomp; $git->dec($_) } <$fh>;
  close $fh
    or croak 404, "Reading tree failed";
  
  # Parse tree
  my @trees;
  for my $line (@entries) {
    my $tree = $git->parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
    $tree->{mode_str} = $git->_mode_str($tree->{mode});
    push @trees, $tree;
  }
  
  # References
  my $refs = $git->references($project);
  
  # Render
  $self->render(
    home => $home,
    home_ns => $home_ns,
    project => $project,
    project_ns => $project_ns,
    dir => $dir,
    id => $id,
    tid => $tid,
    commit => $commit,
    trees => \@trees,
    refs => $refs
  );
}

sub _parse_blobdiff_lines {
  my ($self, $lines_raw) = @_;
  
  # Git
  my $git = $self->app->git;
  
  # Parse
  my @lines;
  for my $line (@$lines_raw) {
    $line = $git->dec($line);
    chomp $line;
    my $class;
    
    if ($line =~ /^diff \-\-git /) { $class = 'diff header' }
    elsif ($line =~ /^index /) { $class = 'diff extended_header' }
    elsif ($line =~ /^\+/) { $class = 'diff to_file' }
    elsif ($line =~ /^\-/) { $class = 'diff from_file' }
    elsif ($line =~ /^\@\@/) { $class = 'diff chunk_header' }
    elsif ($line =~ /^Binary files/) { $class = 'diff binary_file' }
    else { $class = 'diff' }
    push @lines, {value => $line, class => $class};
  }
  
  return \@lines;
}

sub _parse_id_path {
  my ($self, $project, $id_path) = @_;
  
  # Git
  my $git = $self->app->git;
  
  # Parse id and path
  my $refs = $git->references($project);
  my $id;
  my $path;
  for my $rs (values %$refs) {
    for my $ref (@$rs) {
      $ref =~ s#^heads/##;
      $ref =~ s#^tags/##;
      if ($id_path =~ s#^\Q$ref(/|$)##) {
        $id = $ref;
        $path = $id_path;
        last;
      }      
    }
  }
  unless (defined $id) {
    if ($id_path =~ s#(^[^/]+)(/|$)##) {
      $id = $1;
      $path = $id_path;
    }
  }
  
  return ($id, $path);
}

sub _quote_command {
  my $self = shift;
  return join(' ',
    map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
}

1;