Merge pull request #218 from abf/rosa-build:214-delete-and-restore-branches

#214: Delete/Restore branches
This commit is contained in:
avokhmin 2013-07-22 12:41:20 +04:00
commit c33588f620
45 changed files with 685 additions and 181 deletions

View File

@ -59,6 +59,12 @@ gem 'rest-client', '~> 1.6.6'
gem 'attr_encrypted', '1.2.1'
gem "gemoji", "~> 1.2.1", require: 'emoji/railtie'
# AngularJS related stuff
gem 'angularjs-rails'
gem 'ng-rails-csrf'
gem 'momentjs-rails'
gem 'angular-i18n', '0.1.2'
group :assets do
gem 'sass-rails', '~> 3.2.5'
gem 'coffee-rails', '~> 3.2.2'

View File

@ -58,36 +58,37 @@ GEM
json
ancestry (1.3.0)
activerecord (>= 2.3.14)
angular-i18n (0.1.2)
angularjs-rails (1.0.7)
arel (3.0.2)
attr_encrypted (1.2.1)
encryptor (>= 1.1.1)
bcrypt-ruby (3.0.1)
better_errors (0.8.0)
bcrypt-ruby (3.1.1)
better_errors (0.9.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
binding_of_caller (0.7.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (3.1.2)
bluepill (0.0.62)
activesupport (>= 3.0.0)
bluepill (0.0.66)
activesupport (>= 3.0.0, < 4.0.0)
daemons (~> 1.1.4)
i18n (>= 0.5.0)
state_machine (~> 1.1.0)
bourne (1.4.0)
mocha (~> 0.13.2)
state_machine (~> 1.1)
builder (3.0.4)
callsite (0.0.11)
cancan (1.6.7)
cape (1.7.0)
capistrano (2.14.2)
capistrano (2.15.5)
highline
net-scp (>= 1.0.0)
net-sftp (>= 2.0.0)
net-ssh (>= 2.0.14)
net-ssh-gateway (>= 1.1.0)
capistrano_colors (0.5.5)
charlock_holmes (0.6.9.2)
charlock_holmes (0.6.9.4)
chronic (0.6.7)
chunky_png (1.2.7)
chunky_png (1.2.8)
cocaine (0.4.2)
coderay (1.0.9)
coffee-rails (3.2.2)
@ -96,7 +97,7 @@ GEM
coffee-script (2.2.0)
coffee-script-source
execjs
coffee-script-source (1.6.2)
coffee-script-source (1.6.3)
compass (0.12.2)
chunky_png (~> 1.2)
fssm (>= 0.2.7)
@ -106,7 +107,7 @@ GEM
creole (0.5.0)
daemons (1.1.9)
debug_inspector (0.0.2)
devise (2.2.3)
devise (2.2.4)
bcrypt-ruby (~> 3.0)
orm_adapter (~> 0.1)
railties (~> 3.1)
@ -156,9 +157,9 @@ GEM
activesupport (>= 3.1, < 4.1)
haml (~> 3.1)
railties (>= 3.1, < 4.1)
hashie (1.2.0)
highline (1.6.15)
hike (1.2.1)
hashie (2.0.5)
highline (1.6.19)
hike (1.2.3)
hirb (0.7.1)
httpauth (0.2.0)
i18n (0.6.1)
@ -176,11 +177,10 @@ GEM
libv8 (3.3.10.4)
macaddr (1.6.1)
systemu (~> 2.5.0)
mail (2.5.3)
i18n (>= 0.4.0)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mailcatcher (0.5.11)
mailcatcher (0.5.12)
activesupport (~> 3.0)
eventmachine (~> 1.0.0)
haml (>= 3.1, < 5)
@ -191,41 +191,44 @@ GEM
thin (~> 1.5.0)
meta-tags (1.2.6)
actionpack
meta_request (0.2.3)
meta_request (0.2.7)
callsite
rack-contrib
railties
metaclass (0.0.1)
mime-types (1.21)
mocha (0.13.3)
metaclass (~> 0.0.1)
mime-types (1.23)
mini_portile (0.5.1)
mock_redis (0.6.2)
multi_json (1.7.5)
momentjs-rails (2.0.0.2)
railties (>= 3.1)
multi_json (1.7.7)
multipart-post (1.2.0)
mustache (0.99.4)
net-scp (1.1.0)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-sftp (2.1.1)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.6.6)
net-ssh (2.6.8)
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
newrelic_rpm (3.5.5.38)
nokogiri (1.5.9)
ng-rails-csrf (0.1.0)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
oauth2 (0.8.1)
faraday (~> 0.8)
httpauth (~> 0.1)
jwt (~> 0.1.4)
multi_json (~> 1.0)
rack (~> 1.2)
omniauth (1.1.3)
hashie (~> 1.2)
omniauth (1.1.4)
hashie (>= 1.2, < 3)
rack
omniauth-facebook (1.4.1)
omniauth-oauth2 (~> 1.1.0)
omniauth-github (1.1.0)
omniauth-github (1.1.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-google-oauth2 (0.1.13)
omniauth-google-oauth2 (0.2.0)
omniauth (~> 1.0)
omniauth-oauth2
omniauth-oauth2 (1.1.1)
@ -283,13 +286,13 @@ GEM
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
raindrops (0.10.0)
rake (10.0.4)
rdiscount (2.0.7.1)
raindrops (0.11.0)
rake (10.1.0)
rdiscount (2.1.6)
rdoc (3.12.2)
json (~> 1.4)
redcarpet (2.2.2)
redis (3.0.3)
redis (3.0.4)
redis-actionpack (3.2.3)
actionpack (~> 3.2.3)
redis-rack (~> 1.4.0)
@ -297,7 +300,7 @@ GEM
redis-activesupport (3.2.3)
activesupport (~> 3.2.3)
redis-store (~> 1.1.0)
redis-namespace (1.2.1)
redis-namespace (1.3.0)
redis (~> 3.0.0)
redis-rack (1.4.2)
rack (~> 1.4.1)
@ -324,7 +327,7 @@ GEM
actionmailer (~> 3.0)
rest-client (1.6.7)
mime-types (>= 1.16)
rr (1.0.4)
rr (1.0.5)
rspec (2.11.0)
rspec-core (~> 2.11.0)
rspec-expectations (~> 2.11.0)
@ -338,7 +341,7 @@ GEM
activesupport (>= 3.0)
railties (>= 3.0)
rspec (~> 2.11.0)
ruby-haml-js (0.0.3)
ruby-haml-js (0.0.4)
execjs
sprockets (>= 2.0.0)
rubypython (0.5.3)
@ -346,28 +349,27 @@ GEM
ffi (~> 1.0.7)
russian (0.6.0)
i18n (>= 0.5.0)
rvm-capistrano (1.2.7)
rvm-capistrano (1.4.1)
capistrano (>= 2.0.0)
sanitize (2.0.3)
nokogiri (>= 1.4.4, < 1.6)
sass (3.2.7)
sanitize (2.0.6)
nokogiri (>= 1.4.4)
sass (3.2.9)
sass-rails (3.2.6)
railties (~> 3.2.0)
sass (>= 3.1.10)
tilt (~> 1.3)
shotgun (0.9)
rack (>= 1.0)
shoulda (3.4.0)
shoulda (3.5.0)
shoulda-context (~> 1.0, >= 1.0.1)
shoulda-matchers (~> 1.0, >= 1.4.1)
shoulda-context (1.0.2)
shoulda-matchers (1.5.4)
shoulda-matchers (>= 1.4.1, < 3.0)
shoulda-context (1.1.4)
shoulda-matchers (2.2.0)
activesupport (>= 3.0.0)
bourne (~> 1.3)
sinatra (1.3.6)
sinatra (1.4.3)
rack (~> 1.4)
rack-protection (~> 1.3)
tilt (~> 1.3, >= 1.3.3)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
skinny (0.2.3)
eventmachine (~> 1.0.0)
thin (~> 1.5.0)
@ -377,7 +379,7 @@ GEM
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.7)
state_machine (1.1.2)
state_machine (1.2.0)
stringex (1.4.0)
systemu (2.5.2)
therubyracer (0.10.2)
@ -386,9 +388,9 @@ GEM
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thor (0.17.0)
tilt (1.3.6)
treetop (1.4.12)
thor (0.18.1)
tilt (1.4.1)
treetop (1.4.14)
polyglot
polyglot (>= 0.3.1)
turbo-sprockets-rails3 (0.3.6)
@ -407,7 +409,7 @@ GEM
macaddr (~> 1.0)
vegas (0.1.11)
rack (>= 1.0.0)
warden (1.2.1)
warden (1.2.3)
rack (>= 1.0)
whenever (0.7.3)
activesupport (>= 2.3.4)
@ -424,6 +426,8 @@ DEPENDENCIES
RedCloth
airbrake (~> 3.1.2)
ancestry (~> 1.3.0)
angular-i18n (= 0.1.2)
angularjs-rails
attr_encrypted (= 1.2.1)
better_errors
binding_of_caller
@ -453,7 +457,9 @@ DEPENDENCIES
meta-tags (~> 1.2.5)
meta_request
mock_redis (= 0.6.2)
momentjs-rails
newrelic_rpm (~> 3.5.5.38)
ng-rails-csrf
omniauth
omniauth-facebook
omniauth-github

View File

@ -0,0 +1,26 @@
var RosaABF = angular.module('RosaABF', ['ngResource', 'ng-rails-csrf', 'angular-i18n']);
var DateTimeFormatter = function() {
var UtcFormatter = function(api_time) {
return moment.utc(api_time * 1000).format('YYYY-MM-DD HH:mm:ss UTC');
}
return {
utc : UtcFormatter
}
}
RosaABF.factory("DateTimeFormatter", DateTimeFormatter);
var LocalesHelper = function($locale) {
var locales = {
'ru' : 'ru-ru',
'en' : 'en-us'
}
return {
setLocale: function(locale) {
$locale.id = locales[locale];
}
}
}
RosaABF.factory("LocalesHelper", ['$locale', LocalesHelper]);

View File

@ -0,0 +1,70 @@
RosaABF.controller('ProjectRefsController', ['$scope', '$http', 'ApiProject', function($scope, $http, ApiProject) {
$scope.singleton = ApiProject.singleton;
$scope.branches = [];
$scope.tags = [];
$scope.project_id = null;
$scope.current_ref = null;
$scope.project_resource = null;
$scope.init = function(project_id, ref, locale) {
$scope.project_id = project_id;
$scope.current_ref = ref;
$scope.project_resource = ApiProject.resource.get({id: $scope.project_id}, function(results) {
$scope.project = new Project(results.project);
$scope.getRefs();
});
}
$scope.getRefs = function() {
$scope.project_resource.$refs({id: $scope.project_id}, function(results) {
$scope.tags = [];
$scope.branches = [];
_.each(results.refs_list, function(ref){
var result = new ProjectRef(ref);
if (result.isTag) {
if (result.ref == $scope.current_ref) {
$scope.tags.unshift(result);
} else {
$scope.tags.push(result);
}
} else {
if (result.ref == $scope.current_ref) {
$scope.branches.unshift(result);
} else {
$scope.branches.push(result);
}
}
});
$scope.updateBranchesCount();
});
}
$scope.updateBranchesCount = function() {
$scope.singleton.project.branches_count = $scope.branches.length;
}
$scope.destroy = function(branch) {
$scope.project_resource.$delete_branch(
{owner: $scope.project.owner.uname, project: $scope.project.name, ref: branch.ref},
function() { // on success
var i = $scope.branches.indexOf(branch);
if(i != -1) { $scope.branches.splice(i, 1); }
$scope.updateBranchesCount();
// Removes branch from "Current branch/tag:" select box
$('#branch_selector option').filter(function() {
return this.value.match('.*\/branches\/' + branch.ref + '$');
}).remove();
}, function () { // on error
$scope.getRefs();
}
);
}
}]);

View File

@ -0,0 +1,9 @@
RosaABF.controller('ProjectRepoBlockController', ['$scope', 'ApiProject', function($scope, ApiProject) {
$scope.singleton = ApiProject.singleton;
$scope.init = function(branches) {
$scope.singleton.project.branches_count = branches;
}
}]);

View File

@ -0,0 +1,98 @@
RosaABF.controller('PullRequestController',['$scope', '$http', 'ApiPullRequest', 'ApiProject', 'DateTimeFormatter', function($scope, $http, ApiPullRequest, ApiProject, DateTimeFormatter) {
$scope.project_id = null;
$scope.project_resource = null;
$scope.serial_id = null;
$scope.pull = null;
$scope.pull_resource = null;
$scope.merged_at = null;
$scope.closed_at = null;
$scope.branch = null;
$scope.can_delete_branch = false;
$scope.init = function(project_id, serial_id) {
$scope.project_id = project_id;
$scope.serial_id = serial_id;
$scope.getPullRequest();
}
$scope.getPullRequest = function() {
$scope.pull_resource = ApiPullRequest.resource.get(
{project_id: $scope.project_id, serial_id: $scope.serial_id},
function(results) {
$scope.pull = results.pull_request;
if ($scope.pull.merged_at) { $scope.merged_at = DateTimeFormatter.utc($scope.pull.merged_at); }
if ($scope.pull.closed_at) { $scope.closed_at = DateTimeFormatter.utc($scope.pull.closed_at); }
}
);
}
// @param [from_ref] - sets only at first time
$scope.getBranch = function(from_ref) {
if (!$scope.project_resource) {
$scope.project_resource = ApiProject.resource.get({id: $scope.project_id});
}
// Fix: at first load
// Cannot read property 'from_ref' of null
if (!from_ref) { from_ref = $scope.pull.from_ref.ref; }
$scope.project_resource.$refs({id: $scope.project_id}, function(results) {
var branch = null;
_.each(results.refs_list, function(ref){
var result = new ProjectRef(ref);
if (!result.isTag && result.ref == from_ref) {
branch = result;
return true;
}
});
$scope.branch = branch;
});
}
$scope.reopen = function() {
$scope.pull.status = 'reopen';
$scope.pull_resource.$update(function() {
$scope.getPullRequest();
});
}
$scope.close = function() {
$scope.pull.status = 'close';
$scope.pull_resource.$update(function() {
$scope.getPullRequest();
});
}
$scope.merge = function() {
$scope.pull_resource.$merge(function() {
$scope.getPullRequest();
});
}
$scope.deleteBranch = function() {
$scope.project_resource.$delete_branch($scope.branch_params(),
function() { $scope.branch = null; }, // success
function() { $scope.getBranch(); } // error
);
}
$scope.restoreBranch = function() {
$scope.project_resource.$restore_branch($scope.branch_params(),
function() { $scope.getBranch(); }, // success
function() { $scope.getBranch(); } // error
);
}
$scope.branch_params = function() {
var project = $scope.pull.from_ref.project;
return {
owner: project.fullname.replace(/\/.*/, ''),
project: project.name,
ref: $scope.pull.from_ref.ref,
sha: $scope.pull.from_ref.sha
}
}
}]);

View File

@ -0,0 +1,7 @@
RosaABF.controller('RosaABFController', ['$scope', 'LocalesHelper', function($scope, LocalesHelper) {
$scope.init = function(locale) {
LocalesHelper.setLocale(locale);
}
}]);

View File

@ -0,0 +1,24 @@
var _locales = {
'ru-ru': {
'project.total_branches': [
'Всего %1 ветка',
'Всего %1 ветки',
'Всего %1 веток'
],
'project.total_tags': [
'Всего %1 тег',
'Всего %1 тега',
'Всего %1 тегов'
]
},
'en-us': {
'project.total_branches': [
'Total %1 branch',
'Total %1 branches'
],
'project.total_tags': [
'Total %1 tag',
'Total %1 tags'
]
}
};

View File

@ -0,0 +1,14 @@
var Project = function(atts){
var self = this;
var initialSettings = atts || {};
//initial settings if passed in
for(var setting in initialSettings){
if(initialSettings.hasOwnProperty(setting))
self[setting] = initialSettings[setting];
};
//with some logic...
//return the scope-safe instance
return self;
};

View File

@ -0,0 +1,29 @@
var ProjectRef = function(atts) {
var self = this;
var initialSettings = atts || {};
//initial settings if passed in
for(var setting in initialSettings){
if(initialSettings.hasOwnProperty(setting))
self[setting] = initialSettings[setting];
};
//with some logic...
self.isTag = self.object.type == 'tag';
self.path = function(project) {
return '/' + project.fullname + '/tree/' + self.ref;
}
self.diff_path = function(project, current_ref) {
return '/' + project.fullname + '/diff/' + current_ref + '...' + self.ref;
}
self.archive_path = function(project, type) {
return '/' + project.fullname + '/archive/' + project.name + '-' + self.ref + '.' + type;
}
//return the scope-safe instance
return self;
};

View File

@ -0,0 +1,33 @@
RosaABF.factory("ApiProject", ['$resource', function($resource) {
var ProjectResource = $resource(
'/api/v1/projects/:id.json',
{id: '@project.id'},
{
refs: {
url: '/api/v1/projects/:id/refs_list.json',
method: 'GET',
isArray : false
},
delete_branch: {
url: '/:owner/:project/branches/:ref',
method: 'DELETE',
isArray : false
},
restore_branch: {
url: '/:owner/:project/branches/:ref', // ?sha=<sha>
method: 'PUT',
isArray : false
}
}
);
return {
resource : ProjectResource,
singleton : {
project : {
branches_count : 0
}
}
}
}]);

View File

@ -0,0 +1,25 @@
RosaABF.factory("ApiPullRequest", ['$resource', function($resource) {
var PullRequestResource = $resource(
'/api/v1/projects/:project_id/pull_requests/:serial_id.json',
{
project_id: '@pull_request.to_ref.project.id',
serial_id: '@pull_request.number'
},
{
update: {
method: 'PUT',
isArray : false
},
merge: {
url: '/api/v1/projects/:project_id/pull_requests/:serial_id/merge.json',
method: 'PUT',
isArray: false
}
}
);
return {
resource : PullRequestResource
}
}]);

View File

@ -13,6 +13,16 @@
//= require backbone_rails_sync
//= require backbone_datalink
//= require backbone/rosa
// require angular
//= require unstable/angular
// require angular-resource
//= require unstable/angular-resource
//= require ng-rails-csrf
//= require angular-i18n
//= require_tree ./angularjs
//= require moment
//= require_self
function disableNotifierCbx(global_cbx) {

View File

@ -744,7 +744,7 @@ div.admin-role {
padding-right: 10px;
}
article a.right_floated {
.right_floated {
float: right;
}

View File

@ -2,7 +2,7 @@
class Api::V1::ProjectsController < Api::V1::BaseController
before_filter :authenticate_user!
skip_before_filter :authenticate_user!, :only => [:get_id, :show, :refs] if APP_CONFIG['anonymous_access']
skip_before_filter :authenticate_user!, :only => [:get_id, :show, :refs_list] if APP_CONFIG['anonymous_access']
load_and_authorize_resource :project
@ -23,6 +23,8 @@ class Api::V1::ProjectsController < Api::V1::BaseController
end
def refs_list
@refs = @project.repo.branches.sort_by(&:name) +
@project.repo.tags.select{ |t| t.commit }.sort_by(&:name).reverse
end
def update

View File

@ -78,7 +78,7 @@ class Api::V1::PullRequestsController < Api::V1::BaseController
if (action = pull_params[:status]) && %w(close reopen).include?(pull_params[:status])
if @pull.send("can_#{action}?")
@pull.set_user_and_time current_user
need_check = true if action == 'reopen'
need_check = true if action == 'reopen' && @pull.valid?
end
end
end

View File

@ -2,8 +2,8 @@
class Projects::Git::BaseController < Projects::BaseController
before_filter :authenticate_user!
skip_before_filter :authenticate_user!, :only => [:show, :index, :blame, :raw, :archive, :diff, :tags, :branches] if APP_CONFIG['anonymous_access']
load_and_authorize_resource :project
load_and_authorize_resource :project
before_filter :set_treeish_and_path
before_filter :set_branch_and_tree

View File

@ -2,6 +2,10 @@
class Projects::Git::TreesController < Projects::Git::BaseController
before_filter lambda{redirect_to @project if params[:treeish] == @project.default_branch and params[:path].blank?}, :only => :show
skip_before_filter :set_branch_and_tree, :set_treeish_and_path, :only => :archive
before_filter lambda { raise Grit::NoSuchPathError if params[:treeish] != @branch.try(:name) }, :only => [:branch, :destroy]
skip_authorize_resource :project, :only => [:destroy, :restore_branch]
before_filter lambda { authorize!(:write, @project) }, :only => [:destroy, :restore_branch]
def show
render('empty') and return if @project.is_empty?
@ -28,14 +32,19 @@ class Projects::Git::TreesController < Projects::Git::BaseController
end
def tags
@tags = @project.repo.tags.select{ |t| t.commit }.sort_by(&:name).reverse
render 'refs'
end
def restore_branch
@project.restore_branch @treeish, params[:sha] if @treeish.present? && params[:sha].present?
render :nothing => true
end
def destroy
status = @branch && @project.delete_branch(@branch, current_user) ? 200 : 422
render :nothing => true, :status => status
end
def branches
raise Grit::NoSuchPathError if params[:treeish] != @branch.try(:name) # get wrong branch name to nonempty project
@branches = @project.repo.branches.sort_by(&:name).select{ |b| b.name != @branch.name }.unshift(@branch).compact if @branch
render 'refs'
end
end

View File

@ -78,14 +78,6 @@ class Projects::PullRequestsController < Projects::BaseController
redirect_to project_pull_request_path(@pull.to_project, @pull)
end
def merge
unless @pull.merge!(current_user)
flash.now[:error] = t('flash.pull_request.save_error')
flash.now[:warning] = @pull.errors.full_messages.join('. ')
end
redirect_to project_pull_request_path(@pull.to_project, @pull)
end
def show
load_diff_commits_data
end

View File

@ -8,9 +8,9 @@ module PullRequestHelper
(common_comments + pull_comments + commits).sort_by{ |c| c[0] }.map{ |c| c[1] }
end
def pull_status_label pull
def pull_status_label pull_status, options = {}
statuses = {'ready' => 'success', 'closed' => 'important', 'merged' => 'important', 'blocked' => 'warning'}
content_tag :span, t("projects.pull_requests.statuses.#{pull.status}"), :class => "state label-bootstrap label-#{statuses[pull.status]}"
content_tag :span, t("projects.pull_requests.statuses.#{pull_status}"), options.merge(:class => "state label-bootstrap label-#{statuses[pull_status]}")
end
def pull_status pull

View File

@ -15,7 +15,7 @@ class Ability
# Shared rights between guests and registered users
can [:show, :archive], Project, :visibility => 'open'
can :get_id, Project, :visibility => 'open' # api
can :archive, Project, :visibility => 'open'
can(:refs_list, Project) {|project| can? :show, project}
can :read, Issue, :project => {:visibility => 'open'}
can [:read, :commits, :files], PullRequest, :to_project => {:visibility => 'open'}
can :search, BuildList
@ -74,7 +74,6 @@ class Ability
can(:destroy, Project) {|project| project.owner_type == 'Group' and project.owner.actors.exists?(:actor_type => 'User', :actor_id => user.id, :role => 'admin')}
can :remove_user, Project
can :preview, Project
can(:refs_list, Project) {|project| can? :read, project}
can([:read, :create, :edit, :destroy, :update], Hook) {|hook| can?(:edit, hook.project)}

View File

@ -49,6 +49,10 @@ class PullRequest < ActiveRecord::Base
end
end
def cross_pull?
from_project_id != to_project_id
end
def check(do_transaction = true)
if do_transaction && !valid?
issue.set_close nil

View File

@ -109,7 +109,7 @@ class CommentPresenter < ApplicationPresenter
statuses = {'open' => 'success', 'closed' => 'important'}
content_tag :span, t("layout.issues.status.#{@referenced_issue.status}"), :class => "state label-bootstrap label-#{statuses[@referenced_issue.status]}"
else
pull_status_label @referenced_issue
pull_status_label @referenced_issue.status
end.html_safe
end
end

View File

@ -1,8 +1,9 @@
json.refs_list (@project.repo.branches + @project.repo.tags) do |grit|
json.refs_list @refs do |grit|
json.ref grit.name
json.object do
json.type (grit.class.name =~ /Tag/ ? 'tag' : 'commit')
json.sha grit.commit.id
json.authored_date grit.commit.authored_date.to_i
end
end
json.url refs_list_api_v1_project_path(@project.id, :format => :json)

View File

@ -2,14 +2,14 @@ json.number pull.serial_id
json.(pull, :title, :status)
json.to_ref do
json.ref pull.to_ref
json.sha pull.to_commit.id
json.sha pull.to_commit.try(:id)
json.project do
json.partial! 'api/v1/projects/project', :project => pull.to_project
end
end
json.from_ref do
json.ref pull.from_ref
json.sha pull.from_commit.id
json.sha pull.from_commit.try(:id)
json.project do
json.partial! 'api/v1/projects/project', :project => pull.from_project
end

View File

@ -1,7 +1,7 @@
json.issue do
json.pull_request do
json.partial! 'pull', :pull => @pull
json.body @pull.body
json.closed_at pull.issue.closed_at.to_i if @pull.merged? || @pull.closed?
json.closed_at @pull.issue.closed_at.to_i if @pull.merged? || @pull.closed?
json.closed_by do
json.partial! 'api/v1/shared/member', :member => @pull.issue.closer
end if @pull.issue.closer

View File

@ -1,3 +1,3 @@
json.(member, :id, :name)
json.(member, :id, :name, :uname)
json.type member.class.name
json.url member_path(member)
json.url member_path(member)

View File

@ -12,7 +12,7 @@
- if user_signed_in?
= auto_discovery_link_tag :atom, atom_activity_feeds_path(:format => 'atom', :token => current_user.authentication_token), :title => t("layout.atom_link_tag_title", :nickname => current_user.uname, :app_name => APP_CONFIG['project_name'])
%body
%body{'ng-app' => 'RosaABF', 'ng-controller' => 'RosaABFController', 'ng-init' => "init('#{I18n.locale}')"}
.wrap{:class => content_for?(:sidebar) ? 'columns' : ''}
%header
.left

View File

@ -1,7 +1,7 @@
- act = action_name.to_sym; contr = controller_name.to_sym; treeish = project.default_head(params[:treeish]); branch = @branch.try(:name) || project.default_head
-http_url = git_repo_url(project.name_with_owner)
-ssh_url = git_ssh_repo_url(project.name_with_owner)
#description-top
#description-top{'ng-controller' => 'ProjectRepoBlockController', 'ng-init' => "init(#{project.repo.branches.count})"}
-if @commit
%ul.nav.zip
%li#menu-archive.dropdown
@ -42,7 +42,7 @@
%li{:class => ('selected' if act == :index && contr == :commits )}
= link_to t('project_menu.commits'), commits_path(project, treeish)
%li{:class => ('selected' if act == :branches && contr == :trees )}
= link_to t('project_menu.branches', :count => project.repo.branches.count), branches_path(project, branch)
= link_to t('project_menu.branches', :count => '{{singleton.project.branches_count}}'), branches_path(project, branch)
%li.tags{:class => ('selected' if act == :tags && contr == :trees )}
= link_to t('project_menu.tags', :count => project.repo.tags.count), tags_path(project)
.both

View File

@ -1,10 +0,0 @@
- base = branch.name == @branch.name
%tr{:class => (base ? 'base' : '')}
%td.name= link_to branch.name, tree_path(@project, branch.name)
%td.actions
%ul.actions
- if base
%li.text= t('layout.projects.base_branch')
- else
%li
= link_to t('layout.projects.compare'), diff_path(@project, "#{@branch.name}...#{branch.name}")

View File

@ -1,10 +0,0 @@
- subjects_name = action_name.to_s
%h3= t("layout.projects.#{subjects_name}")
- if subject.blank?
%p= t("layout.projects.no_#{subjects_name}")
- elsif subject.count == 1
%p= t("layout.projects.showing_#{subjects_name.singularize}")
- else
%p= t("layout.projects.showing_#{subjects_name}", :count => subject.count)
.both

View File

@ -1,8 +0,0 @@
%li
= link_to t('layout.projects.browse_code'), tree_path(@project, tag.name), :class => 'detail-link'
- file_name = "#{@project.name}-#{tag.name}"
- %w(zip tar.gz).each do |type|
= link_to t('layout.projects.source_code', :type => type), archive_path(@project, file_name, type), :class => 'detail-link'
%p.name
%b= tag.name
.date= tag.commit.authored_date.strftime('%d.%m.%Y')

View File

@ -0,0 +1,34 @@
-set_meta_tags :title => "#{title_object @project}"
= render 'submenu'
= render 'repo_block', :project => @project
%div{'ng-controller' => 'ProjectRefsController',
'ng-init' => "init('#{@project.id}','#{@branch.try(:name)}')"}
%h3= t('layout.projects.branches')
%p{'ng-show' => '!branches.length'}= t('layout.projects.no_branches')
%p{'ng-show' => 'branches.length > 0'} {{'project.total_branches' | i18n:'plural':branches.length}}
.both
%br
%input.mediumheight{'ng-model' => 'query.ref'}
.both
%table#project-branches
%tbody
%tr{'ng-repeat' => 'branch in branches | filter:query', 'ng-class' => '{base: branch.ref == current_ref}'}
%td.name
%a{'ng-href' => '{{branch.path(project)}}' } {{branch.ref}}
%td.actions
%ul.actions
%li.text{'ng-show' => 'branch.ref == current_ref'}
= t('layout.projects.base_branch')
- if can?(:write, @project)
%li{'ng-hide' => 'branch.ref == current_ref'}
%a{:href => '', 'ng-click' => 'destroy(branch)'}
= t('layout.projects.delete_branch')
%li{'ng-hide' => 'branch.ref == current_ref'}
%a{'ng-href' => '{{branch.diff_path(project, current_ref)}}' }
= t('layout.projects.compare')

View File

@ -1,14 +0,0 @@
-set_meta_tags :title => "#{title_object @project}"
= render 'submenu'
= render 'repo_block', :project => @project
= render 'header', :subject => (@branches || @tags)
- if @tags.present?
%div#project-tags
%ol.release-list
= render :partial => 'tag', :collection => @tags
- elsif @branches.present?
%table#project-branches
%tbody
= render :partial => 'branch', :collection => @branches

View File

@ -0,0 +1,27 @@
-set_meta_tags :title => "#{title_object @project}"
= render 'submenu'
= render 'repo_block', :project => @project
%div{'ng-controller' => 'ProjectRefsController', 'ng-init' => "init('#{@project.id}')"}
%h3= t('layout.projects.tags')
%p{'ng-show' => '!tags.length'}= t('layout.projects.no_tags')
%p{'ng-show' => 'tags.length > 0'} {{'project.total_tags' | i18n:'plural':tags.length}}
.both
%br
%input.mediumheight{'ng-model' => 'query.ref'}
.both
#project-tags
%ol.release-list
%li{'ng-repeat' => 'tag in tags | filter:query'}
%a.detail-link{'ng-href' => '{{tag.path(project)}}' }
= t('layout.projects.browse_code')
- %w(zip tar.gz).each do |type|
%a.detail-link{'ng-href' => "{{tag.archive_path(project, '#{type}')}}" }
= t('layout.projects.source_code', :type => type)
%p.name
%b {{tag.ref}}
.date {{tag.object.authored_date * 1000 | date:'yyyy.MM.dd'}}

View File

@ -1,14 +1,36 @@
- if can?(:merge, @pull) && @pull.can_merging?
%br
=form_for PullRequest.new, :url => merge_project_pull_request_path(@project, @pull), :html => { :method => :put, :class => :form } do |f|
=f.submit t 'projects.pull_requests.ready'
-else
.flash
%div{:class => @pull.ready? ? 'notice' : 'alert'}
=pull_status @pull
%a.button{:href => '', 'ng-click' => 'merge()', 'ng-show' => "pull.status == 'ready'"}
= t 'projects.pull_requests.ready'
.both
.flash{'ng-show' => '!pull.mergeable'}
.notice{'ng-show' => "pull.status == 'blocked'"}
= t "projects.pull_requests.blocked"
.alert{'ng-show' => "pull.status == 'merged'"}
= t("projects.pull_requests.merged",
:user => '{{pull.merged_by.uname}}',
:to_ref => show_ref(@pull, 'to'),
:from_ref => show_ref(@pull, 'from'),
:time => '{{merged_at}}').html_safe
.alert{'ng-show' => "pull.status == 'closed'"}
= t("projects.pull_requests.closed",
:user => '{{pull.closed_by.uname}}',
:time => '{{closed_at}}')
.both
- if !@pull.cross_pull? && can?(:write, @project)
%div{'ng-init' => "getBranch('#{@pull.from_ref}')", 'ng-show' => "pull.status == 'closed' || pull.status == 'merged'"}
%a.button{:href => '', 'ng-click' => 'deleteBranch()', 'ng-show' => "branch && branch.object.sha == pull.from_ref.sha"}
= t('layout.projects.delete_branch')
%a.button{:href => '', 'ng-click' => 'restoreBranch()', 'ng-hide' => "branch"}
= t('layout.projects.restore_branch')
.both
-if can? :update, @pull
-if action = @pull.can_close? ? 'close' : ('reopen' if @pull.can_reopen?)
%br
=form_for :pull, :url => [@project, @pull], :html => { :id => 'do_pull_action',:method => :put, :class => :form } do |f|
=hidden_field_tag "pull_request_action", action
=f.submit t ".#{action}"
%br
%a.button{:href => '', 'ng-click' => 'reopen()', 'ng-show' => "pull.status == 'closed'"}
= t '.reopen'
%a.button{:href => '', 'ng-click' => 'close()', 'ng-show' => "pull.status == 'ready' || pull.status == 'open' || pull.status == 'blocked'"}
= t '.close'

View File

@ -1,19 +1,23 @@
-ar = 'activerecord.attributes.pull_requests'
-set_meta_tags :title => [title_object(@project), t('.title', :name => @pull.title.truncate(40), :user => @pull.user.try(:uname))]
= render :partial => 'submenu'
%h3.bpadding10
=pull_status_label @pull
=pull_header @pull
#repo-wrapper
=render 'nav_tabs'
.tab-content.pull_diff_fix
#discussion.tab-pane.active
=render 'projects/issues/header'
=render 'activity'
%br
=render "projects/comments/add", :project => @project, :commentable => @issue if current_user
.pull_status
=render 'status'
=render 'diff_commits_tabs' unless @pull.already?
=hidden_field_tag :preview_url, project_md_preview_path(@project)
= render "projects/comments/markdown_help"
%div{'ng-controller' => 'PullRequestController', 'ng-init' => "init('#{@project.id}', '#{@pull.serial_id}')"}
%h3.bpadding10
- PullRequest::STATUSES.each do |status|
= pull_status_label status, {'ng-show' => "pull.status == '#{status}'"}
= pull_header @pull
#repo-wrapper
=render 'nav_tabs'
.tab-content.pull_diff_fix
#discussion.tab-pane.active
=render 'projects/issues/header'
=render 'activity'
%br
=render "projects/comments/add", :project => @project, :commentable => @issue if current_user
.pull_status
=render 'status'
=render 'diff_commits_tabs' unless @pull.already?
=hidden_field_tag :preview_url, project_md_preview_path(@project)
= render "projects/comments/markdown_help"

View File

@ -2,16 +2,14 @@ en:
layout:
projects:
branches: Branches
showing_branches: Showing %{count} branches
showing_branch: Showing 1 branch
delete_branch: Delete branch
restore_branch: Restore branch
no_branches: No branches
base_branch: Base branch
compare: Compare
browse_code: Browse code
source_code: Source code (%{type})
tags: Tags
showing_tags: Showing %{count} tags
showing_tag: Showing 1 tag
no_tags: No tags
add: Add
public_projects_list: Public projects list

View File

@ -2,16 +2,14 @@ ru:
layout:
projects:
branches: Ветки
showing_branches: Показано %{count} веток
showing_branch: Показана 1 ветка
no_branch: Нет веток
delete_branch: Удалить ветку
restore_branch: Востановить ветку
no_branches: Нет веток
base_branch: Текущая ветка
compare: Сравнить
browse_code: Просмотреть код
source_code: Исходный код (%{type})
tags: Теги
showing_tags: Показано %{count} тегов
showing_tag: Показан 1 тег
no_tags: Нет тегов
add: Добавить
public_projects_list: Список публичных проектов

View File

@ -21,7 +21,7 @@ ru:
ready: Данный пул реквест можно смержить автоматически.
merged: |
%{user} смержил <span class="label-bootstrap label-info font14">%{to_ref}</span>
с <span class="label-bootstrap label-info font14">%{from_ref}</span> в %{time}'
с <span class="label-bootstrap label-info font14">%{from_ref}</span> в %{time}
closed: '%{user} закрыл пул реквест в %{time}'
is_big: Этот пул реквест слишком большой! Мы показываем только последние %{count} коммитов.
open: ''

View File

@ -310,7 +310,6 @@ Rosa::Application.routes.draw do
resources :hooks, :except => :show
resources :pull_requests, :except => :destroy do
get :autocomplete_to_project, :on => :collection
put :merge, :on => :member
end
post '/preview' => 'projects#preview', :as => 'md_preview'
post 'refs_list' => 'projects#refs_list', :as => 'refs_list'
@ -336,6 +335,8 @@ Rosa::Application.routes.draw do
get '/tags' => "git/trees#tags", :as => :tags
# Branches
get '/branches/:treeish' => "git/trees#branches", :as => :branches
delete '/branches/:treeish' => "git/trees#destroy", :as => :branches
put '/branches/:treeish' => "git/trees#restore_branch", :as => :branches
# Commits
get '/commits/:treeish(/*path)' => "git/commits#index", :as => :commits, :format => false
get '/commit/:id(.:format)' => "git/commits#show", :as => :commit

View File

@ -33,6 +33,20 @@ module Modules
repo.tags.map(&:name) + repo.branches.map(&:name)
end
# TODO: return something else instead of empty string on success and error
def restore_branch(branch, sha)
repo.git.native(:branch, {}, branch, sha)
end
def delete_branch(branch, user)
return false if default_branch == branch.name
message = repo.git.native(:branch, {}, '-D', branch.name)
if message.present?
Resque.enqueue(GitHook,owner.uname, name, GitHook::ZERO, branch.commit.id, "refs/heads/#{branch.name}", 'commit', "user-#{user.id}", message)
end
return message.present?
end
def update_file(path, data, options = {})
head = options[:head].to_s || default_branch
actor = get_actor(options[:actor])

View File

@ -7,7 +7,6 @@ describe Projects::Git::TreesController do
stub_symlink_methods
@project = FactoryGirl.create(:project)
@another_user = FactoryGirl.create(:user)
@params = { :owner_name => @project.owner.uname,
:project_name => @project.name,
:treeish => "#{@project.name}-master"}
@ -37,6 +36,17 @@ describe Projects::Git::TreesController do
get :archive, @params.merge(:format => 'tar.gz')
response.code.should == '401'
end
it 'should not be able to perform destroy action' do
delete :destroy, @params.merge(:treeish => 'master')
response.should_not be_success
end
it 'should not be able to perform restore_branch action' do
put :restore_branch, @params.merge(:treeish => 'master')
response.should_not be_success
end
end
context 'for other user' do
@ -60,6 +70,16 @@ describe Projects::Git::TreesController do
response.should be_success
end
it 'should not be able to perform destroy action' do
delete :destroy, @params.merge(:treeish => 'master')
response.should_not be_success
end
it 'should not be able to perform restore_branch action' do
put :restore_branch, @params.merge(:treeish => 'master')
response.should_not be_success
end
[:tags, :branches].each do |action|
it "should be able to perform #{action} action" do
get action, @params.merge(:treeish => 'master')
@ -68,5 +88,28 @@ describe Projects::Git::TreesController do
end
end
context 'for writer user' do
before(:each) do
user = FactoryGirl.create(:user)
@project.relations.create!(:actor_type => 'User', :actor_id => user.id, :role => 'writer')
set_session_for user
end
it 'should be able to perform destroy action' do
delete :destroy, @params.merge(:treeish => 'conflicts')
response.should be_success
end
it 'should not be able to perform destroy action for master branch' do
delete :destroy, @params.merge(:treeish => 'master')
response.should_not be_success
end
it 'should be able to perform restore_branch action' do
put :restore_branch, @params.merge(:treeish => 'conflicts')
response.should be_success
end
end
after(:all) {clean_projects_dir}
end

View File

@ -97,11 +97,6 @@ shared_examples_for 'user with pull request update rights' do
response.should redirect_to(project_pull_request_path(@pull.to_project, @pull))
end
it 'should be able to perform merge action' do
put :merge, @update_params
response.should redirect_to(project_pull_request_path(@pull.to_project, @pull))
end
let(:pull) { @project.pull_requests.find(@pull) }
it 'should update pull request status' do
put :update, @update_params
@ -135,11 +130,6 @@ shared_examples_for 'user without pull request update rights' do
response.should redirect_to(controller.current_user ? forbidden_path : new_user_session_path)
end
it 'should not be able to perform merge action' do
put :merge, @update_params
response.should redirect_to(controller.current_user ? forbidden_path : new_user_session_path)
end
let(:pull) { @project.pull_requests.find(@pull) }
it 'should not update pull request status' do
put :update, @update_params

View File

@ -86,4 +86,55 @@ describe Project do
lambda {FactoryGirl.create(:project, :name => "...\nbeatiful_name\n for project")}.should raise_error(ActiveRecord::RecordInvalid)
end
end
context 'manage branches' do
let!(:project) { FactoryGirl.create(:project_with_commit) }
let(:branch) { project.repo.branches.detect{|b| b.name == 'conflicts'} }
let(:master) { project.repo.branches.detect{|b| b.name == 'master'} }
let(:user) { FactoryGirl.create(:user) }
before { stub_redis }
context '#delete_branch' do
it 'ensures that returns true on success' do
project.delete_branch(branch, user).should be_true
end
it 'ensures that branch has been deleted' do
lambda { project.delete_branch(branch, user) }.should change{ project.repo.branches.count }.by(-1)
end
it 'ensures that returns false on delete master' do
project.delete_branch(master, user).should be_false
end
it 'ensures that master has not been deleted' do
lambda { project.delete_branch(master, user) }.should change{ project.repo.branches.count }.by(0)
end
it 'ensures that returns false on delete wrong branch' do
project.delete_branch(branch, user)
project.delete_branch(branch, user).should be_false
end
end
context '#restore_branch' do
before do
project.delete_branch(branch, user)
end
xit 'ensures that returns true on success' do
project.restore_branch(branch.name, branch.commit.id).should be_true
end
it 'ensures that branch has been restored' do
lambda { project.restore_branch(branch.name, branch.commit.id) }.should change{ project.repo.branches.count }.by(1)
end
xit 'ensures that returns false on restore wrong branch' do
project.restore_branch(branch.name, GitHook::ZERO).should be_false
end
end
end
end