gitprep / lib / Gitprep / Manager.pm /
97bccf3 7 years ago
3 contributor
967 lines | 25.291kb
package Gitprep::Manager;
use Mojo::Base -base;

use Carp 'croak';
use Encode 'encode';
use File::Copy qw/move copy/;
use File::Path qw/mkpath rmtree/;
use File::Temp ();
use Fcntl ':flock';
use Carp 'croak';
use File::Copy qw/copy move/;
use File::Spec;
use Gitprep::Util;

has 'app';
has 'authorized_keys_file';

has '_tmp_branch' => '__gitprep_tmp_branch__';

sub prepare_merge {
  my ($self, $work_rep_info, $base_rep_info, $base_branch, $target_rep_info, $target_branch) = @_;
  
  # Fetch base repository
  my $base_user_id = $base_rep_info->{user};
  my @git_fetch_base_cmd = $self->app->git->cmd($work_rep_info, 'fetch', 'origin');
  Gitprep::Util::run_command(@git_fetch_base_cmd)
    or Carp::croak "Can't execute git fetch: @git_fetch_base_cmd";
  
  # Fetch target repository
  my @git_fetch_target_cmd = $self->app->git->cmd($work_rep_info, 'fetch', $target_rep_info->{git_dir});
  
  Gitprep::Util::run_command(@git_fetch_target_cmd)
    or Carp::croak "Can't execute git fetch: @git_fetch_target_cmd";

  # Ensure no diff
  my @git_reset_hard_cmd = $self->app->git->cmd(
    $work_rep_info,
    'reset',
    '--hard'
  );
  Gitprep::Util::run_command(@git_reset_hard_cmd)
    or Carp::croak "Can't execute git reset --hard: @git_reset_hard_cmd";

  # Checkout first branch
  my $tmp_branch = $self->_tmp_branch;
  my $branch_names = $self->app->git->branch_names($work_rep_info);
  my $first_branch;
  for my $branch_name (@$branch_names) {
    if ($branch_name ne $tmp_branch) {
      $first_branch = $branch_name;
      last;
    }
  }
  my @git_checkout_first_branch = $self->app->git->cmd(
    $work_rep_info,
    'checkout',
    $first_branch
  );
  Gitprep::Util::run_command(@git_checkout_first_branch)
    or Carp::croak "Can't execute git checkout: @git_checkout_first_branch";
  
  # Delete temparary branch if it eixsts
  if (grep { $_ eq $tmp_branch } @$branch_names) {
    my @git_branch_remove_cmd = $self->app->git->cmd(
      $work_rep_info,
      'branch',
      '-D',
      $tmp_branch
    );
    Gitprep::Util::run_command(@git_branch_remove_cmd)
      or Carp::croak "Can't execute git branch: @git_branch_remove_cmd";
  }

  # Create temparary branch
  my @git_branch_cmd = $self->app->git->cmd(
    $work_rep_info,
    'branch',
    $tmp_branch
  );
  Gitprep::Util::run_command(@git_branch_cmd)
    or Carp::croak "Can't execute git branch: @git_branch_cmd";
  
  # Checkout tmp branch and git reset --hard from my remote branch
  my @git_checkout_tmp_branch = $self->app->git->cmd(
    $work_rep_info,
    'checkout',
    $tmp_branch
  );
  Gitprep::Util::run_command(@git_checkout_tmp_branch)
    or Carp::croak "Can't execute git checkout: @git_checkout_tmp_branch";
  
  # git reset --hard 
  my $base_object_id = $self->app->git->ref_to_object_id($base_rep_info, $base_branch);
  my @git_reset_hard_base_cmd = $self->app->git->cmd(
    $work_rep_info,
    'reset',
    '--hard',
    $base_object_id
  );
  Gitprep::Util::run_command(@git_reset_hard_base_cmd)
    or Carp::croak "Can't execute git reset --hard: @git_reset_hard_base_cmd";
}

sub merge {
  my ($self, $work_rep_info, $target_rep_info, $target_branch, $pull_request_number) = @_;
  
  my $object_id = $self->app->git->ref_to_object_id($target_rep_info, $target_branch);
  
  my $message;
  my $target_user_id = $target_rep_info->{user};
  if (defined $pull_request_number) {
    $message = "Merge pull request #$pull_request_number from $target_user_id/$target_branch";
  }
  else {
    $message = "Merge from $target_user_id/$target_branch";
  }
  
  # Merge
  my @git_merge_cmd = $self->app->git->cmd(
    $work_rep_info,
    'merge',
    '--no-ff',
    "--message=$message",
    $object_id
  );
  # 
  
  my $success = Gitprep::Util::run_command(@git_merge_cmd);
  
  return $success;
}

sub get_patch {
  my ($self, $work_rep_info, $target_rep_info, $target_branch) = @_;
  
  my $object_id = $self->app->git->ref_to_object_id($target_rep_info, $target_branch);
  
  # Merge
  my @git_format_patch_cmd = $self->app->git->cmd(
    $work_rep_info,
    'format-patch',
    '--stdout',
    "HEAD..$object_id"
  );
  
  open my $fh, '-|', @git_format_patch_cmd
    or croak "Execute git format-patch cmd:@git_format_patch_cmd";
  
  my $patch = do { local $/; <$fh> };
  
  return $patch;
}

sub push {
  my ($self, $work_rep_info, $base_branch) = @_;
  
  # Push
  my $tmp_branch = $self->_tmp_branch;
  my @git_push_cmd = $self->app->git->cmd(
    $work_rep_info,
    'push',
    'origin',
    "$tmp_branch:$base_branch"
  );
  Gitprep::Util::run_command(@git_push_cmd)
    or Carp::croak "Can't execute git push: @git_push_cmd";
}

sub lock_rep {
  my ($self, $rep_info) = @_;
  
  my $git_dir = $rep_info->{git_dir};
  my $lock_file = "$git_dir/config";
  
  open my $lock_fh, '<', $lock_file
    or croak "Can't open lock file $lock_file: $!";
    
  flock $lock_fh, LOCK_EX
    or croak "Can't lock $lock_file";
  
  return $lock_fh;
}

sub create_work_rep {
  my ($self, $user, $project) = @_;
  
  # Remote repository
  my $rep_info = $self->app->rep_info($user, $project);
  my $rep_git_dir = $rep_info->{git_dir};
  
  # Working repository
  my $work_rep_info = $self->app->work_rep_info($user, $project);
  my $work_tree = $work_rep_info->{work_tree};
  
  # Create working repository if it don't exist
  unless (-e $work_tree) {

    # git clone
    my @git_clone_cmd = ($self->app->git->bin, 'clone', $rep_git_dir, $work_tree);
    Gitprep::Util::run_command(@git_clone_cmd)
      or croak "Can't git clone: @git_clone_cmd";
    
    # Set user name
    my @git_config_user_name = $self->app->git->cmd(
      $work_rep_info,
      'config',
      'user.name',
      $user
    );
    Gitprep::Util::run_command(@git_config_user_name)
      or croak "Can't execute git config: @git_config_user_name";
    
    # Set user email
    my $user_email = $self->app->dbi->model('user')->select('email', where => {id => $user})->value;
    my @git_config_user_email = $self->app->git->cmd(
      $work_rep_info,
      'config',
      'user.email',
      "$user_email"
    );
    Gitprep::Util::run_command(@git_config_user_email)
      or croak "Can't execute git config: @git_config_user_email";
  }
}

sub admin_user {
  my $self = shift;
  
  # Admin user
  my $admin_user = $self->app->dbi->model('user')
    ->select(where => {admin => 1})->one;
  
  return $admin_user;
}

sub default_branch {
  my ($self, $user_id, $project_id, $default_branch) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Set default branch
  my $dbi = $self->app->dbi;
  if (defined $default_branch) {
    $dbi->model('project')->update(
      {default_branch => $default_branch},
      where => {user => $user_row_id, id => $project_id}
    );
  }
  else {
    # Get default branch
    my $default_branch = $dbi->model('project')
      ->select('default_branch', where => {user => $user_row_id, id => $project_id})
      ->value;
    
    return $default_branch;
  }
}

sub fork_project {
  my ($self, $forked_user_id, $user_id, $project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Fork project
  my $dbi = $self->app->dbi;
  my $error;
  eval {
    $dbi->connector->txn(sub {
      
      # Original project id
      my $project = $dbi->model('project')->select(
        {__MY__ => ['row_id', 'private']},
        where => {'user.id' => $user_id, 'project.id' => $project_id}
      )->one;
      
      # Create project
      eval {
        $self->_create_project(
          $forked_user_id,
          $project_id,
          {
            original_project => $project->{row_id},
            private => $project->{private}
          }
        );
      };
      croak $error = $@ if $@;
      
      # Create repository
      eval {
        $self->_fork_rep($user_id, $project_id, $forked_user_id, $project_id);
      };
      croak $error = $@ if $@;
    });
  };
  croak $error if $@;
}

sub is_admin {
  my ($self, $user_id) = @_;
  
  # Check admin
  my $is_admin = $self->app->dbi->model('user')
    ->select('admin', where => {id => $user_id})->value;
  
  return $is_admin;
}

sub is_private_project {
  my ($self, $user_id, $project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Is private
  my $private = $self->app->dbi->model('project')->select(
    'private', where => {user => $user_row_id, id => $project_id}
  )->value;
  
  return $private;
}

sub api { shift->app->gitprep_api }


sub member_projects {
  my ($self, $user_id, $project_id) = @_;
  
  # DBI
  my $dbi = $self->app->dbi;
  
  # project id
  my $project_row_id = $dbi->model('project')->select(
    'project.row_id',
    where => {'user.id' => $user_id, 'project.id' => $project_id}
  )->value;
  
  # Members
  my $member_projects = $dbi->model('project')->select(
    [
      {__MY__ => ['id']},
      {user => ['id']}
    ],
    where => {
      original_project => $project_row_id,
    },
    append => 'order by user.id, project.id'
  )->all;

  return $member_projects;
}

sub create_project {
  my ($self, $user_id, $project_id, $opts) = @_;
  
  my $params = {};
  if ($opts->{private}) {
    $params->{private} = 1;
  }
  
  # Create project
  my $dbi = $self->app->dbi;
  my $error;
  eval {
    $dbi->connector->txn(sub {
      eval { $self->_create_project($user_id, $project_id, $params) };
      croak $error = $@ if $@;
      eval {$self->_create_rep($user_id, $project_id, $opts) };
      croak $error = $@ if $@;
    });
  };
  croak $error if $@;
}

sub create_user {
  my ($self, $user, $data) = @_;

  # Create user
  my $dbi = $self->app->dbi;
  my $error;
  eval {
    $dbi->connector->txn(sub {
      eval { $self->_create_db_user($user, $data) };
      croak $error = $@ if $@;
      eval {$self->_create_user_dir($user) };
      croak $error = $@ if $@;
    });
  };
  croak $error if $@;
}

sub delete_project {
  my ($self, $user, $project) = @_;
  
  # Delete project
  my $dbi = $self->app->dbi;
  my $error;
  eval {
    $dbi->connector->txn(sub {
      eval { $self->_delete_project($user, $project) };
      croak $error = $@ if $@;
      eval {$self->_delete_rep($user, $project) };
      croak $error = $@ if $@;
    });
  };
  croak $error if $@;
}

sub delete_user {
  my ($self, $user) = @_;
  
  # Delete user
  my $dbi = $self->app->dbi;
  my $error;
  my $count;
  eval {
    $dbi->connector->txn(sub {
      eval { $count = $self->_delete_db_user($user) };
      croak $error = $@ if $@;
      eval {$self->_delete_user_dir($user) };
      croak $error = $@ if $@;
    });
  };
  croak $error if $@;
  
  return $count;
}

sub original_project {
  my ($self, $user_id, $project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Original project id
  my $dbi = $self->app->dbi;
  my $original_project_row_id = $dbi->model('project')->select(
    'original_project',
    where => {user => $user_row_id, id => $project_id}
  )->value;
  
  croak "Original project don't eixsts." unless defined $original_project_row_id;
  
  # Original project
  my $original_project = $dbi->model('project')->select(
    [
      {__MY__ => '*'},
      {user => ['id']}
    ],
    where => {
      'project.row_id' => $original_project_row_id
    }
  )->one;
  
  return unless defined $original_project;
  
  return $original_project;
}

sub child_project {
  my ($self, $user_id, $project_id, $child_user_id) = @_;
  
  my $project_row_id = $self->app->dbi->model('project')->select(
    'project.row_id', where => {'user.id' => $user_id, 'project.id' => $project_id}
  )->value;
  
  my $child_project = $self->app->dbi->model('project')->select(
    [
      {__MY__ => '*'},
      {user => ['id']}
    ],
    where => {
      'project.original_project' => $project_row_id,
      'user.id' => $child_user_id
    }
  )->one;
  
  return $child_project;
}

sub projects {
  my ($self, $user_id) = @_;
  
  my $user_row_id = $self->app->dbi->model('user')->select('row_id', where => {id => $user_id})->value;
  
  # Projects
  my $projects = $self->app->dbi->model('project')->select(
    where => {user => $user_row_id},
    append => 'order by id'
  )->all;
  
  return $projects;
}

sub users {
  my $self = shift;
  
  # Users
  my $users = $self->app->dbi->model('user')->select(
    where => [':admin{<>}',{admin => 1}],
    append => 'order by id'
  )->all;
  
  return $users;
}

sub rename_project {
  my ($self, $user, $project, $to_project) = @_;
  
  # Rename project
  my $git = $self->app->git;
  my $dbi = $self->app->dbi;
  my $error;
  eval {
    $dbi->connector->txn(sub {
      eval { $self->_rename_project($user, $project, $to_project) };
      croak $error = $@ if $@;
      eval { $self->_rename_rep($user, $project, $to_project) };
      croak $error = $@ if $@;
    });
  };
  croak $error if $error;
}

sub update_authorized_keys_file {
  my $self = shift;

  my $authorized_keys_file = $self->authorized_keys_file;
  if (defined $authorized_keys_file) {
    
    # Lock file
    my $lock_file = $self->app->home->rel_file('lock/authorized_keys');
    open my $lock_fh, $lock_file
      or croak "Can't open lock file $lock_file";
    flock $lock_fh, LOCK_EX
      or croak "Can't lock $lock_file";
    
    # Create authorized_keys_file
    unless (-f $authorized_keys_file) {
      open my $fh, '>', $authorized_keys_file
        or croak "Can't create authorized_keys file: $authorized_keys_file";
      chmod 0600, $authorized_keys_file
        or croak "Can't chmod authorized_keys file: $authorized_keys_file";
    }
    
    # Parse file
    my $result = $self->parse_authorized_keys_file($authorized_keys_file);
    my $before_part = $result->{before_part};
    my $gitprep_part = $result->{gitprep_part};
    my $after_part = $result->{after_part};
    my $start_symbol = $result->{start_symbol};
    my $end_symbol = $result->{end_symbol};
    
    # Backup at first time
    if ($gitprep_part eq '') {
      # Backup original file
      my $to = "$authorized_keys_file.gitprep.original";
      unless (-f $to) {
        copy $authorized_keys_file, $to
          or croak "Can't copy $authorized_keys_file to $to";
      }
    }

    # Create public keys
    my $ssh_public_keys = $self->app->dbi->model('ssh_public_key')->select(
      [
        {__MY__ => '*'},
        {user => ['id']}
      ]
    )->all;
    my $ssh_public_keys_str = '';
    for my $key (@$ssh_public_keys) {
      my $ssh_public_key_str = 'command="' . $self->app->home->rel_file('script/gitprep-shell')
        . " $key->{'user.id'}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $key->{key}";
      $ssh_public_keys_str .= "$ssh_public_key_str $key->{'user.id'}\n\n";
    }
    
    # Output tmp file
    my $output = "$before_part$start_symbol\n\n$ssh_public_keys_str$end_symbol$after_part";
    my $output_file = "$authorized_keys_file.gitprep.tmp";
    open my $out_fh, '>', $output_file
      or croak "Can't create authorized_keys tmp file $output_file";
    print $out_fh $output;
    close $out_fh
      or croak "Can't close authorized_keys tmp file $output_file";

    # Replace
    chmod 0600, $output_file
      or croak "Can't chmod authorized_keys tmp file: $output_file";
    move $output_file, $authorized_keys_file
      or croak "Can't replace $authorized_keys_file by $output_file";
  }
  else {
    croak qq/authorized_keys file "$authorized_keys_file" is not found./;
  }
}

sub parse_authorized_keys_file {
  my ($self, $file) = @_;
  
  my $start_symbol = "# gitprep start";
  my $end_symbol = "# gitprep end";
  
  # Parse
  open my $fh, '<', $file
    or croak "Can't open authorized_key file $file";
  my $start_symbol_count = 0;
  my $end_symbol_count = 0;
  my $before_part = '';
  my $gitprep_part = '';
  my $after_part = '';
  my $error_prefix = "authorized_keys file $file format error:";
  while (my $line = <$fh>) {
    if ($line =~ /^$start_symbol/) {
      if ($start_symbol_count > 0) {
        croak qq/$error_prefix "$start_symbol" is found more than one/;
      }
      else {
        if ($end_symbol_count > 0) {
          croak qq/$error_prefix "$end_symbol" is found before "$start_symbol"/;
        }
        else {
          $start_symbol_count++;
        }
      }
    }
    elsif ($line =~ /^$end_symbol/) {
      if ($end_symbol_count > 0) {
        croak qq/$error_prefix "$end_symbol" is found more than one/;
      }
      else {
        $end_symbol_count++;
      }
    }
    elsif ($start_symbol_count == 0 && $end_symbol_count == 0) {
      $before_part .= $line;
    }
    elsif ($start_symbol_count == 1 && $end_symbol_count == 0) {
      $gitprep_part .= $line;
    }
    elsif ($start_symbol_count == 1 && $end_symbol_count == 1) {
      $after_part .= $line;
    }
  }
  
  my $result = {
    start_symbol => $start_symbol,
    end_symbol => $end_symbol,
    before_part => $before_part,
    gitprep_part => $gitprep_part,
    after_part => $after_part
  };
  
  return $result;
}

sub _create_project {
  my ($self, $user_id, $project_id, $params) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  $params ||= {};
  $params->{user} = $user_row_id;
  $params->{id} = $project_id;
  
  # Create project
  my $dbi = $self->app->dbi;
  $dbi->connector->txn(sub {
    $dbi->model('project')->insert($params);
  });
}

sub _create_rep {
  my ($self, $user, $project, $opts) = @_;
  
  # Create repository directory
  my $git = $self->app->git;
  
  my $rep_info = $self->app->rep_info($user, $project);
  my $rep_git_dir = $rep_info->{git_dir};
  
  mkdir $rep_git_dir
    or croak "Can't create directory $rep_git_dir: $!";
  
  eval {
    # Git init
    {
      my @git_init_cmd = $git->cmd($rep_info, 'init', '--bare');
      Gitprep::Util::run_command(@git_init_cmd)
        or croak  "Can't execute git init --bare:@git_init_cmd";
    }
    
    # Add git-daemon-export-ok
    {
      my $file = "$rep_git_dir/git-daemon-export-ok";
      open my $fh, '>', $file
        or croak "Can't create git-daemon-export-ok: $!"
    }
    
    # HTTP support
    my @git_update_server_info_cmd = $git->cmd(
      $rep_info,
      '--bare',
      'update-server-info'
    );
    Gitprep::Util::run_command(@git_update_server_info_cmd)
      or croak "Can't execute git --bare update-server-info";
    move("$rep_git_dir/hooks/post-update.sample", "$rep_git_dir/hooks/post-update")
      or croak "Can't move post-update";
    
    # Description
    my $description = $opts->{description};
    $description = '' unless defined $description;
    {
      my $file = "$rep_git_dir/description";
      open my $fh, '>', $file
        or croak "Can't open $file: $!";
      print $fh encode('UTF-8', $description)
        or croak "Can't write $file: $!";
      close $fh;
    }
    
    # Add README and commit
    if ($opts->{readme}) {
      # Create working directory
      my $home_tmp_dir = $self->app->home->rel_file('tmp');
      
      # Temp directory
      my $temp_dir =  File::Temp->newdir(DIR => $home_tmp_dir);
      
      # Working repository
      my $work_rep_work_tree = "$temp_dir/work";
      my $work_rep_git_dir = "$work_rep_work_tree/.git";
      my $work_rep_info = {
        work_tree => $work_rep_work_tree,
        git_dir => $work_rep_git_dir
      };
      
      mkdir $work_rep_work_tree
        or croak "Can't create directory $work_rep_work_tree: $!";
      
      # Git init
      my @git_init_cmd = $git->cmd($work_rep_info, 'init', '-q');
      Gitprep::Util::run_command(@git_init_cmd)
        or croak "Can't execute git init: @git_init_cmd";
      
      # Add README
      my $file = "$work_rep_work_tree/README.md";
      open my $readme_fh, '>', $file
        or croak "Can't create $file: $!";
      print $readme_fh "# $project\n";
      print $readme_fh "\n" . encode('UTF-8', $description) . "\n";
      close $readme_fh;
      
      my @git_add_cmd = $git->cmd(
        $work_rep_info,
        'add',
        'README.md'
      );
      
      Gitprep::Util::run_command(@git_add_cmd)
        or croak "Can't execute git add: @git_add_cmd";
      
      # Set user name
      my @git_config_user_name = $git->cmd(
        $work_rep_info,
        'config',
        'user.name',
        $user
      );
      Gitprep::Util::run_command(@git_config_user_name)
        or croak "Can't execute git config: @git_config_user_name";
      
      # Set user email
      my $user_email = $self->app->dbi->model('user')->select('email', where => {id => $user})->value;
      my @git_config_user_email = $git->cmd(
        $work_rep_info,
        'config',
        'user.email',
        "$user_email"
      );
      Gitprep::Util::run_command(@git_config_user_email)
        or croak "Can't execute git config: @git_config_user_email";
      
      # Commit
      my @git_commit_cmd = $git->cmd(
        $work_rep_info,
        'commit',
        '-q',
        '-m',
        'first commit'
      );
      Gitprep::Util::run_command(@git_commit_cmd)
        or croak "Can't execute git commit: @git_commit_cmd";
      
      # Push
      {
        my @git_push_cmd = $git->cmd(
          $work_rep_info,
          'push',
          '-q',
          $rep_git_dir,
          'master'
        );
        # (This is bad, but --quiet option can't supress in old git)
        Gitprep::Util::run_command(@git_push_cmd)
          or croak "Can't execute git push: @git_push_cmd";
      }
    }
  };
  if (my $e = $@) {
    rmtree $rep_git_dir;
    croak $e;
  }
}

sub _create_db_user {
  my ($self, $user_id, $data) = @_;
  
  $data->{id} = $user_id;
  
  # Create database user
  $self->app->dbi->model('user')->insert($data);
}

sub _create_user_dir {
  my ($self, $user) = @_;
  
  # Create user directory
  my $rep_home = $self->app->rep_home;
  my $user_dir = "$rep_home/$user";
  mkpath $user_dir;
}

sub _delete_db_user {
  my ($self, $user_id) = @_;
  
  # Delete database user
  my $count = $self->app->dbi->model('user')->delete(where => {id => $user_id});
  
  return $count;
}

sub _delete_user_dir {
  my ($self, $user) = @_;
  
  # Delete user directory
  my $rep_home = $self->app->rep_home;
  my $user_dir = "$rep_home/$user";
  rmtree $user_dir;
}

sub _delete_project {
  my ($self, $user_id, $project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Delete project
  my $dbi = $self->app->dbi;
  $dbi->model('project')->delete(where => {user => $user_row_id, id => $project_id});
}

sub _delete_rep {
  my ($self, $user, $project) = @_;

  # Delete repository
  my $rep_home = $self->app->rep_home;
  croak "Can't remove repository. repository home is empty"
    if !defined $rep_home || $rep_home eq '';
  my $rep = "$rep_home/$user/$project.git";
  rmtree $rep;
  croak "Can't remove repository. repository is rest"
    if -e $rep;
}

sub exists_project {
  my ($self, $user_id, $project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Exists project
  my $dbi = $self->app->dbi;
  my $row = $dbi->model('project')->select(where => {user => $user_row_id, id => $project_id})->one;
  
  return $row ? 1 : 0;
}

sub exists_user {
  my ($self, $user_id) = @_;
  
  # Exists project
  my $row = $self->app->dbi->model('user')->select(where => {id => $user_id})->one;
  
  return $row ? 1 : 0;
}

sub _exists_rep {
  my ($self, $user, $project) = @_;
  
  # Exists repository
  my $rep_info = $self->app->rep_info($user, $project);
  my $rep_git_dir = $rep_info->{git_dir};
  
  return -e $rep_git_dir;
}

sub _fork_rep {
  my ($self, $user_id, $project_id, $to_user_id, $to_project_id) = @_;
  
  # Fork repository
  my $git = $self->app->git;
  
  my $rep_info = $self->app->rep_info($user_id, $project_id);
  my $rep_git_dir = $rep_info->{git_dir};
  
  my $to_rep_info = $self->app->rep_info($to_user_id, $to_project_id);
  my $to_rep_git_dir = $to_rep_info->{git_dir};

  my @cmd = (
    $git->bin,
    'clone',
    '-q',
    '--bare',
    $rep_git_dir,
    $to_rep_git_dir
  );
  Gitprep::Util::run_command(@cmd)
    or croak "Can't fork repository(_fork_rep): @cmd";
  
  # Copy description
  copy "$rep_git_dir/description", "$to_rep_git_dir/description"
    or croak "Can't copy description file(_fork_rep)";
}

sub _rename_project {
  my ($self, $user_id, $project_id, $renamed_project_id) = @_;
  
  my $user_row_id = $self->api->get_user_row_id($user_id);
  
  # Check arguments
  croak "Invalid parameters(_rename_project)"
    unless defined $user_id && defined $project_id && defined $renamed_project_id;
  
  # Rename project
  my $dbi = $self->app->dbi;
  $dbi->model('project')->update(
    {id => $renamed_project_id},
    where => {user => $user_row_id, id => $project_id}
  );
}

sub _rename_rep {
  my ($self, $user, $project, $renamed_project) = @_;
  
  # Check arguments
  croak "Invalid user name or project"
    unless defined $user && defined $project && defined $renamed_project;

  # Rename repository
  my $rep_info = $self->app->rep_info($user, $project);
  my $rep_git_dir = $rep_info->{git_dir};
  
  my $renamed_rep_info = $self->app->rep_info($user, $renamed_project);
  my $renamed_rep_git_dir = $renamed_rep_info->{git_dir};

  move($rep_git_dir, $renamed_rep_git_dir)
    or croak "Can't move $rep_git_dir to $renamed_rep_git_dir: $!";
}

1;