Commit 77bfc591bf5836892be26059d92411f9fbf04be9

Authored by Alex Denisov
2 parents 85db51f6 b565f334

Merge 'master' branch

Showing 65 changed files with 965 additions and 435 deletions   Show diff stats
@@ -44,7 +44,8 @@ gem "ffaker" @@ -44,7 +44,8 @@ gem "ffaker"
44 gem "seed-fu" 44 gem "seed-fu"
45 45
46 # Markdown to HTML 46 # Markdown to HTML
47 -gem "redcarpet", "~> 2.1.1" 47 +gem "redcarpet", "~> 2.1.1"
  48 +gem "github-markup", "~> 0.7.4"
48 49
49 # Servers 50 # Servers
50 gem "thin" 51 gem "thin"
@@ -108,7 +108,7 @@ GEM @@ -108,7 +108,7 @@ GEM
108 bcrypt-ruby (3.0.1) 108 bcrypt-ruby (3.0.1)
109 blankslate (2.1.2.4) 109 blankslate (2.1.2.4)
110 bootstrap-sass (2.0.4.0) 110 bootstrap-sass (2.0.4.0)
111 - builder (3.0.0) 111 + builder (3.0.2)
112 capybara (1.1.2) 112 capybara (1.1.2)
113 mime-types (>= 1.16) 113 mime-types (>= 1.16)
114 nokogiri (>= 1.3.3) 114 nokogiri (>= 1.3.3)
@@ -125,7 +125,7 @@ GEM @@ -125,7 +125,7 @@ GEM
125 charlock_holmes (0.6.8) 125 charlock_holmes (0.6.8)
126 childprocess (0.3.2) 126 childprocess (0.3.2)
127 ffi (~> 1.0.6) 127 ffi (~> 1.0.6)
128 - chosen-rails (0.9.8) 128 + chosen-rails (0.9.8.3)
129 railties (~> 3.0) 129 railties (~> 3.0)
130 thor (~> 0.14) 130 thor (~> 0.14)
131 coderay (1.0.6) 131 coderay (1.0.6)
@@ -178,6 +178,7 @@ GEM @@ -178,6 +178,7 @@ GEM
178 gherkin (2.11.0) 178 gherkin (2.11.0)
179 json (>= 1.4.6) 179 json (>= 1.4.6)
180 git (1.2.5) 180 git (1.2.5)
  181 + github-markup (0.7.4)
181 gitlab_meta (2.9) 182 gitlab_meta (2.9)
182 grape (0.2.1) 183 grape (0.2.1)
183 hashie (~> 1.2) 184 hashie (~> 1.2)
@@ -397,6 +398,7 @@ DEPENDENCIES @@ -397,6 +398,7 @@ DEPENDENCIES
397 ffaker 398 ffaker
398 foreman 399 foreman
399 git 400 git
  401 + github-markup (~> 0.7.4)
400 gitlab_meta (= 2.9) 402 gitlab_meta (= 2.9)
401 gitolite! 403 gitolite!
402 grack! 404 grack!
app/assets/javascripts/issues.js
@@ -5,7 +5,7 @@ function switchToNewIssue(form){ @@ -5,7 +5,7 @@ function switchToNewIssue(form){
5 $('select#issue_milestone_id').chosen(); 5 $('select#issue_milestone_id').chosen();
6 $("#new_issue_dialog").show("fade", { direction: "right" }, 150); 6 $("#new_issue_dialog").show("fade", { direction: "right" }, 150);
7 $('.top-tabs .add_new').hide(); 7 $('.top-tabs .add_new').hide();
8 - disableButtonIfEmtpyField("#issue_title", ".save-btn"); 8 + disableButtonIfEmptyField("#issue_title", ".save-btn");
9 }); 9 });
10 } 10 }
11 11
@@ -16,7 +16,7 @@ function switchToEditIssue(form){ @@ -16,7 +16,7 @@ function switchToEditIssue(form){
16 $('select#issue_milestone_id').chosen(); 16 $('select#issue_milestone_id').chosen();
17 $("#edit_issue_dialog").show("fade", { direction: "right" }, 150); 17 $("#edit_issue_dialog").show("fade", { direction: "right" }, 150);
18 $('.add_new').hide(); 18 $('.add_new').hide();
19 - disableButtonIfEmtpyField("#issue_title", ".save-btn"); 19 + disableButtonIfEmptyField("#issue_title", ".save-btn");
20 }); 20 });
21 } 21 }
22 22
@@ -80,6 +80,10 @@ function issuesPage(){ @@ -80,6 +80,10 @@ function issuesPage(){
80 $(this).closest("form").submit(); 80 $(this).closest("form").submit();
81 }); 81 });
82 82
  83 + $("#new_issue_link").click(function(){
  84 + updateNewIssueURL();
  85 + });
  86 +
83 $('body').on('ajax:success', '.close_issue, .reopen_issue, #new_issue', function(){ 87 $('body').on('ajax:success', '.close_issue, .reopen_issue, #new_issue', function(){
84 var t = $(this), 88 var t = $(this),
85 totalIssues, 89 totalIssues,
@@ -126,3 +130,20 @@ function issuesCheckChanged() { @@ -126,3 +130,20 @@ function issuesCheckChanged() {
126 $('.issues_filters').show(); 130 $('.issues_filters').show();
127 } 131 }
128 } 132 }
  133 +
  134 +function updateNewIssueURL(){
  135 + var new_issue_link = $("#new_issue_link");
  136 + var milestone_id = $("#milestone_id").val();
  137 + var assignee_id = $("#assignee_id").val();
  138 + var new_href = "";
  139 + if(milestone_id){
  140 + new_href = "issue[milestone_id]=" + milestone_id + "&";
  141 + }
  142 + if(assignee_id){
  143 + new_href = new_href + "issue[assignee_id]=" + assignee_id;
  144 + }
  145 + if(new_href.length){
  146 + new_href = new_issue_link.attr("href") + "?" + new_href;
  147 + new_issue_link.attr("href", new_href);
  148 + }
  149 +};
app/assets/javascripts/main.js
@@ -1,130 +0,0 @@ @@ -1,130 +0,0 @@
1 -$(document).ready(function(){  
2 -  
3 - $(".one_click_select").live("click", function(){  
4 - $(this).select();  
5 - });  
6 -  
7 - $('body').on('ajax:complete, ajax:beforeSend, submit', 'form', function(e){  
8 - var buttons = $('[type="submit"]', this);  
9 - switch( e.type ){  
10 - case 'ajax:beforeSend':  
11 - case 'submit':  
12 - buttons.attr('disabled', 'disabled');  
13 - break;  
14 - case ' ajax:complete':  
15 - default:  
16 - buttons.removeAttr('disabled');  
17 - break;  
18 - }  
19 - })  
20 -  
21 - $(".account-box").mouseenter(showMenu);  
22 - $(".account-box").mouseleave(resetMenu);  
23 -  
24 - $("#projects-list .project").live('click', function(e){  
25 - if(e.target.nodeName != "A" && e.target.nodeName != "INPUT") {  
26 - location.href = $(this).attr("url");  
27 - e.stopPropagation();  
28 - return false;  
29 - }  
30 - });  
31 -  
32 - /**  
33 - * Focus search field by pressing 's' key  
34 - */  
35 - $(document).keypress(function(e) {  
36 - if( $(e.target).is(":input") ) return;  
37 - switch(e.which) {  
38 - case 115: focusSearch();  
39 - e.preventDefault();  
40 - }  
41 - });  
42 -  
43 - /**  
44 - * Commit show suppressed diff  
45 - *  
46 - */  
47 - $(".supp_diff_link").bind("click", function() {  
48 - showDiff(this);  
49 - });  
50 -  
51 - /**  
52 - * Note markdown preview  
53 - *  
54 - */  
55 - $(document).on('click', '#preview-link', function(e) {  
56 - $('#preview-note').text('Loading...');  
57 -  
58 - var previewLinkText = ($(this).text() == 'Preview' ? 'Edit' : 'Preview');  
59 - $(this).text(previewLinkText);  
60 -  
61 - var note = $('#note_note').val();  
62 - if (note.trim().length === 0) { note = 'Nothing to preview'; }  
63 - $.post($(this).attr('href'), {note: note}, function(data) {  
64 - $('#preview-note').html(data);  
65 - });  
66 -  
67 - $('#preview-note, #note_note').toggle();  
68 - e.preventDefault();  
69 - });  
70 -});  
71 -  
72 -function focusSearch() {  
73 - $("#search").focus();  
74 -}  
75 -  
76 -function updatePage(data){  
77 - $.ajax({type: "GET", url: location.href, data: data, dataType: "script"});  
78 -}  
79 -  
80 -function showMenu() {  
81 - $(this).toggleClass('hover');  
82 -}  
83 -  
84 -function resetMenu() {  
85 - $(this).removeClass("hover");  
86 -}  
87 -  
88 -function slugify(text) {  
89 - return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase();  
90 -}  
91 -  
92 -function showDiff(link) {  
93 - $(link).next('table').show();  
94 - $(link).remove();  
95 -}  
96 -  
97 -(function($){  
98 - var _chosen = $.fn.chosen;  
99 - $.fn.extend({  
100 - chosen: function(options) {  
101 - var default_options = {'search_contains' : 'true'};  
102 - $.extend(default_options, options);  
103 - return _chosen.apply(this, [default_options]);  
104 - }})  
105 -})(jQuery);  
106 -  
107 -  
108 -function ajaxGet(url) {  
109 - $.ajax({type: "GET", url: url, dataType: "script"});  
110 -}  
111 -  
112 -/**  
113 - * Disable button if text field is empty  
114 - */  
115 -function disableButtonIfEmtpyField(field_selector, button_selector) {  
116 - field = $(field_selector);  
117 - if(field.val() == "") {  
118 - field.closest("form").find(button_selector).attr("disabled", "disabled").addClass("disabled");  
119 - }  
120 -  
121 - field.on('keyup', function(){  
122 - var field = $(this);  
123 - var closest_submit = field.closest("form").find(button_selector);  
124 - if(field.val() == "") {  
125 - closest_submit.attr("disabled", "disabled").addClass("disabled");  
126 - } else {  
127 - closest_submit.removeAttr("disabled").removeClass("disabled");  
128 - }  
129 - })  
130 -}  
app/assets/javascripts/main.js.coffee 0 → 100644
@@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
  1 +window.updatePage = (data) ->
  2 + $.ajax({type: "GET", url: location.href, data: data, dataType: "script"})
  3 +
  4 +window.slugify = (text) ->
  5 + text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
  6 +
  7 +window.ajaxGet = (url) ->
  8 + $.ajax({type: "GET", url: url, dataType: "script"})
  9 +
  10 + # Disable button if text field is empty
  11 +window.disableButtonIfEmptyField = (field_selector, button_selector) ->
  12 + field = $(field_selector)
  13 + closest_submit = field.closest("form").find(button_selector)
  14 +
  15 + closest_submit.disable() if field.val() is ""
  16 +
  17 + field.on "keyup", ->
  18 + if $(this).val() is ""
  19 + closest_submit.disable()
  20 + else
  21 + closest_submit.enable()
  22 +
  23 +$ ->
  24 + # Click a .one_click_select field, select the contents
  25 + $(".one_click_select").live 'click', -> $(this).select()
  26 +
  27 + # Disable form buttons while a form is submitting
  28 + $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
  29 + buttons = $('[type="submit"]', this)
  30 +
  31 + switch e.type
  32 + when 'ajax:beforeSend', 'submit'
  33 + buttons.disable()
  34 + else
  35 + buttons.enable()
  36 +
  37 + # Show/Hide the profile menu when hovering the account box
  38 + $('.account-box').hover -> $(this).toggleClass('hover')
  39 +
  40 + # Focus search field by pressing 's' key
  41 + $(document).keypress (e) ->
  42 + # Don't do anything if typing in an input
  43 + return if $(e.target).is(":input")
  44 +
  45 + switch e.which
  46 + when 115
  47 + $("#search").focus()
  48 + e.preventDefault()
  49 +
  50 + # Commit show suppressed diff
  51 + $(".supp_diff_link").bind "click", ->
  52 + $(this).next('table').show()
  53 + $(this).remove()
  54 +
  55 + # Note markdown preview
  56 + $(document).on 'click', '#preview-link', (e) ->
  57 + $('#preview-note').text('Loading...')
  58 +
  59 + previewLinkText = if $(this).text() == 'Preview' then 'Edit' else 'Preview'
  60 + $(this).text(previewLinkText)
  61 +
  62 + note = $('#note_note').val()
  63 +
  64 + if note.trim().length == 0
  65 + $('#preview-note').text("Nothing to preview.")
  66 + else
  67 + $.post $(this).attr('href'), {note: note}, (data) ->
  68 + $('#preview-note').html(data)
  69 +
  70 + $('#preview-note, #note_note').toggle()
  71 + e.preventDefault()
  72 + false
  73 +
  74 +(($) ->
  75 + _chosen = $.fn.chosen
  76 + $.fn.extend chosen: (options) ->
  77 + default_options = search_contains: "true"
  78 + $.extend default_options, options
  79 + _chosen.apply this, [default_options]
  80 +
  81 + # Disable an element and add the 'disabled' Bootstrap class
  82 + $.fn.extend disable: ->
  83 + $(this).attr('disabled', 'disabled').addClass('disabled')
  84 +
  85 + # Enable an element and remove the 'disabled' Bootstrap class
  86 + $.fn.extend enable: ->
  87 + $(this).removeAttr('disabled').removeClass('disabled')
  88 +
  89 +)(jQuery)
app/assets/javascripts/note.js
@@ -25,14 +25,14 @@ var NoteList = { @@ -25,14 +25,14 @@ var NoteList = {
25 $(this).closest('li').fadeOut(); }); 25 $(this).closest('li').fadeOut(); });
26 26
27 $(".note-form-holder").live("ajax:before", function(){ 27 $(".note-form-holder").live("ajax:before", function(){
28 - $(".submit_note").attr("disabled", "disabled"); 28 + $(".submit_note").disable()
29 }) 29 })
30 30
31 $(".note-form-holder").live("ajax:complete", function(){ 31 $(".note-form-holder").live("ajax:complete", function(){
32 - $(".submit_note").removeAttr("disabled"); 32 + $(".submit_note").enable()
33 }) 33 })
34 34
35 - disableButtonIfEmtpyField(".note-text", ".submit_note"); 35 + disableButtonIfEmptyField(".note-text", ".submit_note");
36 36
37 $(".note-text").live("focus", function(){ 37 $(".note-text").live("focus", function(){
38 $(this).css("height", "80px"); 38 $(this).css("height", "80px");
@@ -177,6 +177,6 @@ var PerLineNotes = { @@ -177,6 +177,6 @@ var PerLineNotes = {
177 form.show(); 177 form.show();
178 return false; 178 return false;
179 }); 179 });
180 - disableButtonIfEmtpyField(".line-note-text", ".submit_inline_note"); 180 + disableButtonIfEmptyField(".line-note-text", ".submit_inline_note");
181 } 181 }
182 } 182 }
app/assets/javascripts/projects.js.coffee
@@ -8,7 +8,7 @@ window.Projects = -> @@ -8,7 +8,7 @@ window.Projects = ->
8 $('.save-project-loader').show() 8 $('.save-project-loader').show()
9 9
10 $('form #project_default_branch').chosen() 10 $('form #project_default_branch').chosen()
11 - disableButtonIfEmtpyField '#project_name', '.project-submit' 11 + disableButtonIfEmptyField '#project_name', '.project-submit'
12 12
13 # Git clone panel switcher 13 # Git clone panel switcher
14 $ -> 14 $ ->
app/assets/stylesheets/common.scss
@@ -179,6 +179,14 @@ span.update-author { @@ -179,6 +179,14 @@ span.update-author {
179 &.merged { 179 &.merged {
180 background-color: #2A2; 180 background-color: #2A2;
181 } 181 }
  182 +
  183 + &.joined {
  184 + background-color: #1cb9ff;
  185 + }
  186 +
  187 + &.left {
  188 + background-color: #ff5057;
  189 + }
182 } 190 }
183 191
184 form { 192 form {
app/controllers/application_controller.rb
@@ -11,15 +11,11 @@ class ApplicationController < ActionController::Base @@ -11,15 +11,11 @@ class ApplicationController < ActionController::Base
11 helper_method :abilities, :can? 11 helper_method :abilities, :can?
12 12
13 rescue_from Gitlab::Gitolite::AccessDenied do |exception| 13 rescue_from Gitlab::Gitolite::AccessDenied do |exception|
14 - render "errors/gitolite", layout: "error"  
15 - end  
16 -  
17 - rescue_from Gitlab::Gitolite::InvalidKey do |exception|  
18 - render "errors/invalid_ssh_key", layout: "error" 14 + render "errors/gitolite", layout: "error", status: 500
19 end 15 end
20 16
21 rescue_from Encoding::CompatibilityError do |exception| 17 rescue_from Encoding::CompatibilityError do |exception|
22 - render "errors/encoding", layout: "error", status: 404 18 + render "errors/encoding", layout: "error", status: 500
23 end 19 end
24 20
25 rescue_from ActiveRecord::RecordNotFound do |exception| 21 rescue_from ActiveRecord::RecordNotFound do |exception|
app/controllers/issues_controller.rb
@@ -37,7 +37,7 @@ class IssuesController < ApplicationController @@ -37,7 +37,7 @@ class IssuesController < ApplicationController
37 end 37 end
38 38
39 def new 39 def new
40 - @issue = @project.issues.new 40 + @issue = @project.issues.new(params[:issue])
41 respond_with(@issue) 41 respond_with(@issue)
42 end 42 end
43 43
app/controllers/refs_controller.rb
  1 +require 'github/markup'
  2 +
1 class RefsController < ApplicationController 3 class RefsController < ApplicationController
2 include Gitlab::Encode 4 include Gitlab::Encode
3 before_filter :project 5 before_filter :project
app/controllers/team_members_controller.rb
@@ -17,13 +17,12 @@ class TeamMembersController &lt; ApplicationController @@ -17,13 +17,12 @@ class TeamMembersController &lt; ApplicationController
17 end 17 end
18 18
19 def create 19 def create
20 - @team_member = UsersProject.new(params[:team_member])  
21 - @team_member.project = project  
22 - if @team_member.save  
23 - redirect_to team_project_path(@project)  
24 - else  
25 - render "new"  
26 - end 20 + @project.add_users_ids_to_team(
  21 + params[:user_ids],
  22 + params[:project_access]
  23 + )
  24 +
  25 + redirect_to team_project_path(@project)
27 end 26 end
28 27
29 def update 28 def update
app/decorators/event_decorator.rb
@@ -8,7 +8,9 @@ class EventDecorator &lt; ApplicationDecorator @@ -8,7 +8,9 @@ class EventDecorator &lt; ApplicationDecorator
8 "#{self.author_name} #{self.action_name} MR ##{self.target_id}:" + self.merge_request_title 8 "#{self.author_name} #{self.action_name} MR ##{self.target_id}:" + self.merge_request_title
9 elsif self.push? 9 elsif self.push?
10 "#{self.author_name} #{self.push_action_name} #{self.ref_type} " + self.ref_name 10 "#{self.author_name} #{self.push_action_name} #{self.ref_type} " + self.ref_name
11 - else 11 + elsif self.membership_changed?
  12 + "#{self.author_name} #{self.action_name} #{self.project.name}"
  13 + else
12 "" 14 ""
13 end 15 end
14 end 16 end
app/helpers/gitlab_markdown_helper.rb
@@ -27,7 +27,7 @@ module GitlabMarkdownHelper @@ -27,7 +27,7 @@ module GitlabMarkdownHelper
27 filter_html: true, 27 filter_html: true,
28 with_toc_data: true, 28 with_toc_data: true,
29 hard_wrap: true) 29 hard_wrap: true)
30 - @markdown ||= Redcarpet::Markdown.new(gitlab_renderer, 30 + @markdown = Redcarpet::Markdown.new(gitlab_renderer,
31 # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use 31 # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
32 no_intra_emphasis: true, 32 no_intra_emphasis: true,
33 tables: true, 33 tables: true,
app/helpers/projects_helper.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +module ProjectsHelper
  2 + def grouper_project_members(project)
  3 + @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access)
  4 + end
  5 +end
  6 +
app/helpers/tree_helper.rb
@@ -24,4 +24,14 @@ module TreeHelper @@ -24,4 +24,14 @@ module TreeHelper
24 content.name 24 content.name
25 end 25 end
26 end 26 end
  27 +
  28 + # Public: Determines if a given filename is compatible with GitHub::Markup.
  29 + #
  30 + # filename - Filename string to check
  31 + #
  32 + # Returns boolean
  33 + def markup?(filename)
  34 + filename.end_with?(*%w(.mdown .md .markdown .textile .rdoc .org .creole
  35 + .mediawiki .rst .asciidoc .pod))
  36 + end
27 end 37 end
app/models/event.rb
@@ -10,6 +10,8 @@ class Event &lt; ActiveRecord::Base @@ -10,6 +10,8 @@ class Event &lt; ActiveRecord::Base
10 Pushed = 5 10 Pushed = 5
11 Commented = 6 11 Commented = 6
12 Merged = 7 12 Merged = 7
  13 + Joined = 8 # User joined project
  14 + Left = 9 # User left project
13 15
14 belongs_to :project 16 belongs_to :project
15 belongs_to :target, polymorphic: true 17 belongs_to :target, polymorphic: true
@@ -37,7 +39,7 @@ class Event &lt; ActiveRecord::Base @@ -37,7 +39,7 @@ class Event &lt; ActiveRecord::Base
37 # - new issue 39 # - new issue
38 # - merge request 40 # - merge request
39 def allowed? 41 def allowed?
40 - push? || issue? || merge_request? 42 + push? || issue? || merge_request? || membership_changed?
41 end 43 end
42 44
43 def push? 45 def push?
@@ -84,6 +86,18 @@ class Event &lt; ActiveRecord::Base @@ -84,6 +86,18 @@ class Event &lt; ActiveRecord::Base
84 [Closed, Reopened].include?(action) 86 [Closed, Reopened].include?(action)
85 end 87 end
86 88
  89 + def joined?
  90 + action == Joined
  91 + end
  92 +
  93 + def left?
  94 + action == Left
  95 + end
  96 +
  97 + def membership_changed?
  98 + joined? || left?
  99 + end
  100 +
87 def issue 101 def issue
88 target if target_type == "Issue" 102 target if target_type == "Issue"
89 end 103 end
@@ -101,6 +115,10 @@ class Event &lt; ActiveRecord::Base @@ -101,6 +115,10 @@ class Event &lt; ActiveRecord::Base
101 "closed" 115 "closed"
102 elsif merged? 116 elsif merged?
103 "merged" 117 "merged"
  118 + elsif joined?
  119 + 'joined'
  120 + elsif left?
  121 + 'left'
104 else 122 else
105 "opened" 123 "opened"
106 end 124 end
app/models/merge_request.rb
@@ -162,7 +162,7 @@ class MergeRequest &lt; ActiveRecord::Base @@ -162,7 +162,7 @@ class MergeRequest &lt; ActiveRecord::Base
162 end 162 end
163 163
164 def automerge!(current_user) 164 def automerge!(current_user)
165 - if Gitlab::Merge.new(self, current_user).merge 165 + if Gitlab::Merge.new(self, current_user).merge && self.unmerged_commits.empty?
166 self.merge!(current_user.id) 166 self.merge!(current_user.id)
167 true 167 true
168 end 168 end
app/models/users_project.rb
@@ -20,6 +20,23 @@ class UsersProject &lt; ActiveRecord::Base @@ -20,6 +20,23 @@ class UsersProject &lt; ActiveRecord::Base
20 20
21 delegate :name, :email, to: :user, prefix: true 21 delegate :name, :email, to: :user, prefix: true
22 22
  23 + def self.bulk_delete(project, user_ids)
  24 + UsersProject.transaction do
  25 + UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
  26 + users_project.destroy
  27 + end
  28 + end
  29 + end
  30 +
  31 + def self.bulk_update(project, user_ids, project_access)
  32 + UsersProject.transaction do
  33 + UsersProject.where(:user_id => user_ids, :project_id => project.id).each do |users_project|
  34 + users_project.project_access = project_access
  35 + users_project.save
  36 + end
  37 + end
  38 + end
  39 +
23 def self.bulk_import(project, user_ids, project_access) 40 def self.bulk_import(project, user_ids, project_access)
24 UsersProject.transaction do 41 UsersProject.transaction do
25 user_ids.each do |user_id| 42 user_ids.each do |user_id|
app/observers/users_project_observer.rb
@@ -3,4 +3,20 @@ class UsersProjectObserver &lt; ActiveRecord::Observer @@ -3,4 +3,20 @@ class UsersProjectObserver &lt; ActiveRecord::Observer
3 return if users_project.destroyed? 3 return if users_project.destroyed?
4 Notify.project_access_granted_email(users_project.id).deliver 4 Notify.project_access_granted_email(users_project.id).deliver
5 end 5 end
  6 +
  7 + def after_create(users_project)
  8 + Event.create(
  9 + project_id: users_project.project.id,
  10 + action: Event::Joined,
  11 + author_id: users_project.user.id
  12 + )
  13 + end
  14 +
  15 + def after_destroy(users_project)
  16 + Event.create(
  17 + project_id: users_project.project.id,
  18 + action: Event::Left,
  19 + author_id: users_project.user.id
  20 + )
  21 + end
6 end 22 end
app/roles/push_event.rb
@@ -90,6 +90,8 @@ module PushEvent @@ -90,6 +90,8 @@ module PushEvent
90 90
91 def push_with_commits? 91 def push_with_commits?
92 md_ref? && commits.any? && parent_commit && last_commit 92 md_ref? && commits.any? && parent_commit && last_commit
  93 + rescue Grit::NoSuchPathError
  94 + false
93 end 95 end
94 96
95 def last_push_to_non_root? 97 def last_push_to_non_root?
app/roles/team.rb
@@ -36,4 +36,17 @@ module Team @@ -36,4 +36,17 @@ module Team
36 UsersProject.bulk_import(self, users_ids, access_role) 36 UsersProject.bulk_import(self, users_ids, access_role)
37 self.update_repository 37 self.update_repository
38 end 38 end
  39 +
  40 + # Update multiple project users
  41 + # to same access role by user ids
  42 + def update_users_ids_to_role(users_ids, access_role)
  43 + UsersProject.bulk_update(self, users_ids, access_role)
  44 + self.update_repository
  45 + end
  46 +
  47 + # Delete multiple users from project by user ids
  48 + def delete_users_ids_from_team(users_ids)
  49 + UsersProject.bulk_delete(self, users_ids)
  50 + self.update_repository
  51 + end
39 end 52 end
app/views/errors/encoding.html.haml
1 -.alert-message.block-message.error  
2 - %h3 Encoding Error  
3 - %hr  
4 - %p  
5 - Page can't be loaded because of an encoding error. 1 +%h1 Encoding Error
  2 +%hr
  3 +%p Page can't be loaded because of an encoding error.
app/views/errors/invalid_ssh_key.html.haml
@@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
1 -%h1 Git Error  
2 -%hr  
3 -%p Seems like SSH Key you provided is not a valid SSH key.  
app/views/events/_event.html.haml
@@ -11,3 +11,7 @@ @@ -11,3 +11,7 @@
11 .event_feed 11 .event_feed
12 = render "events/event_push", event: event 12 = render "events/event_push", event: event
13 13
  14 + - elsif event.membership_changed?
  15 + .event_feed
  16 + = render "events/event_membership_changed", event: event
  17 +
app/views/events/_event_membership_changed.html.haml 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 += image_tag gravatar_icon(event.author_email), class: "avatar"
  2 +%strong #{event.author_name}
  3 +%span.event_label{class: event.action_name}= event.action_name
  4 +project
  5 +%strong= link_to event.project.name, event.project
  6 +%span.cgray
  7 + = time_ago_in_words(event.created_at)
  8 + ago.
  9 +
app/views/help/markdown.html.haml
@@ -20,6 +20,15 @@ @@ -20,6 +20,15 @@
20 %li milestones 20 %li milestones
21 %li wiki pages 21 %li wiki pages
22 22
  23 + .span4
  24 + .alert.alert-info
  25 + %p
  26 + If you're not already familiar with Markdown, you should spend 15 minutes and go over the excellent
  27 + %strong= link_to "Markdown Syntax Guide", "http://daringfireball.net/projects/markdown/syntax"
  28 + at Daring Fireball.
  29 +
  30 +.row
  31 + .span8
23 %h3 Differences from traditional Markdown 32 %h3 Differences from traditional Markdown
24 33
25 %h4 Newlines 34 %h4 Newlines
@@ -62,6 +71,29 @@ @@ -62,6 +71,29 @@
62 %p becomes 71 %p becomes
63 = markdown %Q{```ruby\nrequire 'redcarpet'\nmarkdown = Redcarpet.new("Hello World!")\nputs markdown.to_html\n```} 72 = markdown %Q{```ruby\nrequire 'redcarpet'\nmarkdown = Redcarpet.new("Hello World!")\nputs markdown.to_html\n```}
64 73
  74 + %h4 Emoji
  75 +
  76 +.row
  77 + .span8
  78 + :ruby
  79 + puts markdown %Q{Sometimes you want to be :cool: and add some :sparkles: to your :speech_balloon:. Well we have a :gift: for you:
  80 +
  81 + :exclamation: You can use emoji anywhere GFM is supported. :sunglasses:
  82 +
  83 + You can use it to point out a :bug: or warn about :monkey:patches. And if someone improves your really :snail: code, send them a :bouquet: or some :candy:. People will :heart: you for that.
  84 +
  85 + If you are :new: to this, don't be :fearful:. You can easily join the emoji :circus_tent:. All you need to do is to :book: up on the supported codes.
  86 + }
  87 +
  88 + .span4
  89 + .alert.alert-info
  90 + %p
  91 + Consult the
  92 + %strong= link_to "Emoji Cheat Sheet", "http://www.emoji-cheat-sheet.com/"
  93 + for a list of all supported emoji codes.
  94 +
  95 +.row
  96 + .span8
65 %h4 Special GitLab references 97 %h4 Special GitLab references
66 98
67 %p 99 %p
@@ -93,12 +125,5 @@ @@ -93,12 +125,5 @@
93 %p For example in your #{link_to @project.name, project_path(@project)} project, writing: 125 %p For example in your #{link_to @project.name, project_path(@project)} project, writing:
94 %pre= "This is related to ##{issue.id}. @#{current_user.name} is working on solving it." 126 %pre= "This is related to ##{issue.id}. @#{current_user.name} is working on solving it."
95 %p becomes: 127 %p becomes:
96 - %pre= gfm "This is related to ##{issue.id}. @#{current_user.name} is working on solving it." 128 + = markdown "This is related to ##{issue.id}. @#{current_user.name} is working on solving it."
97 - @project = nil # Prevent this from bubbling up to page title 129 - @project = nil # Prevent this from bubbling up to page title
98 -  
99 - .span4.right  
100 - .alert.alert-info  
101 - %p  
102 - If you're not already familiar with Markdown, you should spend 15 minutes and go over the excellent  
103 - %strong= link_to "Markdown Syntax Guide", "http://daringfireball.net/projects/markdown/syntax"  
104 - at Daring Fireball.  
app/views/issues/index.html.haml
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 .right 6 .right
7 .span5 7 .span5
8 - if can? current_user, :write_issue, @project 8 - if can? current_user, :write_issue, @project
9 - = link_to new_project_issue_path(@project), class: "right btn", title: "New Issue", remote: true do 9 + = link_to new_project_issue_path(@project), class: "right btn", title: "New Issue", remote: true, id: "new_issue_link" do
10 %i.icon-plus 10 %i.icon-plus
11 New Issue 11 New Issue
12 = form_tag search_project_issues_path(@project), method: :get, remote: true, id: "issue_search_form", class: :right do 12 = form_tag search_project_issues_path(@project), method: :get, remote: true, id: "issue_search_form", class: :right do
app/views/keys/index.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 = link_to "Add new", new_key_path, class: "btn right" 3 = link_to "Add new", new_key_path, class: "btn right"
4 4
5 %hr 5 %hr
6 -%p.slead 6 +%p.slead
7 SSH key allows you to establish a secure connection between your computer and GitLab 7 SSH key allows you to establish a secure connection between your computer and GitLab
8 8
9 9
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 %th 15 %th
16 - @keys.each do |key| 16 - @keys.each do |key|
17 = render(partial: 'show', locals: {key: key}) 17 = render(partial: 'show', locals: {key: key})
18 - - if @keys.blank? 18 + - if @keys.blank?
19 %tr 19 %tr
20 %td{colspan: 3} 20 %td{colspan: 3}
21 %h3.nothing_here_message There are no SSH keys with access to your account. 21 %h3.nothing_here_message There are no SSH keys with access to your account.
app/views/layouts/_head_panel.html.haml
@@ -34,12 +34,4 @@ @@ -34,12 +34,4 @@
34 source: #{raw search_autocomplete_source}, 34 source: #{raw search_autocomplete_source},
35 select: function(event, ui) { location.href = ui.item.url } 35 select: function(event, ui) { location.href = ui.item.url }
36 }); 36 });
37 -  
38 - $(document).keypress(function(e) {  
39 - if($(e.target).is(":input")) return;  
40 - switch(e.which) {  
41 - case 115: focusSearch();  
42 - e.preventDefault();  
43 - }  
44 - });  
45 }); 37 });
app/views/merge_requests/_form.html.haml
@@ -60,7 +60,7 @@ @@ -60,7 +60,7 @@
60 60
61 :javascript 61 :javascript
62 $(function(){ 62 $(function(){
63 - disableButtonIfEmtpyField("#merge_request_title", ".save-btn"); 63 + disableButtonIfEmptyField("#merge_request_title", ".save-btn");
64 $('select#merge_request_assignee_id').chosen(); 64 $('select#merge_request_assignee_id').chosen();
65 $('select#merge_request_source_branch').chosen(); 65 $('select#merge_request_source_branch').chosen();
66 $('select#merge_request_target_branch').chosen(); 66 $('select#merge_request_target_branch').chosen();
app/views/milestones/_form.html.haml
@@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
41 41
42 :javascript 42 :javascript
43 $(function() { 43 $(function() {
44 - disableButtonIfEmtpyField("#milestone_title", ".save-btn"); 44 + disableButtonIfEmptyField("#milestone_title", ".save-btn");
45 $( ".datepicker" ).datepicker({ 45 $( ".datepicker" ).datepicker({
46 dateFormat: "yy-mm-dd", 46 dateFormat: "yy-mm-dd",
47 onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } 47 onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
app/views/projects/_team.html.haml
1 -%table  
2 - %thead  
3 - %tr  
4 - %th User  
5 - %th Permissions  
6 - %tbody  
7 - - @project.users_projects.each do |up|  
8 - = render(partial: 'team_members/show', locals: {member: up}) 1 +- grouper_project_members(@project).each do |access, members|
  2 + %table
  3 + %thead
  4 + %tr
  5 + %th.span7
  6 + = Project.access_options.key(access).pluralize
  7 + %th
  8 + %tbody
  9 + - members.each do |up|
  10 + = render(partial: 'team_members/show', locals: {member: up})
9 11
10 12
11 :javascript 13 :javascript
app/views/refs/_tree.html.haml
@@ -43,11 +43,7 @@ @@ -43,11 +43,7 @@
43 %i.icon-file 43 %i.icon-file
44 = content.name 44 = content.name
45 .file_content.wiki 45 .file_content.wiki
46 - - if content.name =~ /\.(md|markdown)$/i  
47 - = preserve do  
48 - = markdown(content.data)  
49 - - else  
50 - = simple_format(content.data) 46 + = raw GitHub::Markup.render(content.name, content.data)
51 47
52 :javascript 48 :javascript
53 $(function(){ 49 $(function(){
app/views/refs/_tree_file.html.haml
@@ -9,10 +9,9 @@ @@ -9,10 +9,9 @@
9 = link_to "history", project_commits_path(@project, path: params[:path], ref: @ref), class: "btn very_small" 9 = link_to "history", project_commits_path(@project, path: params[:path], ref: @ref), class: "btn very_small"
10 = link_to "blame", blame_file_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small" 10 = link_to "blame", blame_file_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small"
11 - if file.text? 11 - if file.text?
12 - - if name =~ /\.(md|markdown)$/i 12 + - if markup?(name)
13 .file_content.wiki 13 .file_content.wiki
14 - = preserve do  
15 - = markdown(file.data) 14 + = raw GitHub::Markup.render(name, file.data)
16 - else 15 - else
17 .file_content.code 16 .file_content.code
18 - unless file.empty? 17 - unless file.empty?
app/views/team_members/_form.html.haml
1 -%h3= "New Team member" 1 +%h3.page_title
  2 + = "New Team member(s)"
2 %hr 3 %hr
3 = form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f| 4 = form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f|
4 -if @team_member.errors.any? 5 -if @team_member.errors.any?
@@ -7,27 +8,23 @@ @@ -7,27 +8,23 @@
7 - @team_member.errors.full_messages.each do |msg| 8 - @team_member.errors.full_messages.each do |msg|
8 %li= msg 9 %li= msg
9 10
  11 + %h6 1. Choose people you want in the team
10 .clearfix 12 .clearfix
11 - = f.label :user_id, "Name"  
12 - .input= f.select(:user_id, User.not_in_project(@project).all.collect {|p| [ p.name, p.id ] }, { include_blank: "Select user" }, { style: "width:300px" }) 13 + = f.label :user_ids, "Peolpe"
  14 + .input= select_tag(:user_ids, options_from_collection_for_select(User.not_in_project(@project).all, :id, :name), { class: "xxlarge", multiple: true })
13 15
14 16
  17 + %h6 2. Set access level for them
15 .clearfix 18 .clearfix
16 = f.label :project_access, "Project Access" 19 = f.label :project_access, "Project Access"
17 - .input= f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select" 20 + .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select"
18 21
19 22
20 .actions 23 .actions
21 - = f.submit 'Save', class: "btn primary"  
22 - = link_to "Cancel", team_project_path(@project), class: "btn" 24 + = f.submit 'Save', class: "btn save-btn"
  25 + = link_to "Cancel", team_project_path(@project), class: "btn cancel-btn"
23 26
24 -:css  
25 - form select {  
26 - width:300px;  
27 - }  
28 27
29 :javascript 28 :javascript
30 - $('select#team_member_user_id').chosen();  
31 - $('select#team_member_project_access').chosen();  
32 - //$('select#team_member_repo_access').chosen();  
33 - //$('select#team_member_project_access').chosen(); 29 + $('select#user_ids').chosen();
  30 + $('select#project_access').chosen();
app/views/team_members/_show.html.haml
@@ -2,12 +2,6 @@ @@ -2,12 +2,6 @@
2 - allow_admin = can? current_user, :admin_project, @project 2 - allow_admin = can? current_user, :admin_project, @project
3 %tr{id: dom_id(member), class: "team_member_row user_#{user.id}"} 3 %tr{id: dom_id(member), class: "team_member_row user_#{user.id}"}
4 %td 4 %td
5 - .right  
6 - - if @project.owner == user  
7 - %span.label Project Owner  
8 - - if user.blocked  
9 - %span.label Blocked  
10 -  
11 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do 5 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do
12 = image_tag gravatar_icon(user.email, 40), class: "avatar s32" 6 = image_tag gravatar_icon(user.email, 40), class: "avatar s32"
13 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do 7 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do
@@ -16,5 +10,11 @@ @@ -16,5 +10,11 @@
16 %div.cgray= user.email 10 %div.cgray= user.email
17 11
18 %td 12 %td
19 - = form_for(member, as: :team_member, url: project_team_member_path(@project, member)) do |f|  
20 - = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select", disabled: !allow_admin 13 + .right
  14 + - if @project.owner == user
  15 + %span.btn.disabled.success Project Owner
  16 + - if user.blocked
  17 + %span.btn.disabled.blocked Blocked
  18 + - if allow_admin
  19 + = form_for(member, as: :team_member, url: project_team_member_path(@project, member)) do |f|
  20 + = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select"
config/gitlab.yml.example
@@ -33,11 +33,12 @@ app: @@ -33,11 +33,12 @@ app:
33 git_host: 33 git_host:
34 admin_uri: git@localhost:gitolite-admin 34 admin_uri: git@localhost:gitolite-admin
35 base_path: /home/git/repositories/ 35 base_path: /home/git/repositories/
36 - # hooks_path: /var/lib/gitolite/.gitolite/hooks/ # only needed when gitolite is not installed according the manual  
37 - # host: localhost 36 + hooks_path: /home/git/.gitolite/hooks/
  37 + gitolite_admin_key: gitlab
38 git_user: git 38 git_user: git
39 upload_pack: true 39 upload_pack: true
40 receive_pack: true 40 receive_pack: true
  41 + # host: localhost
41 # port: 22 42 # port: 22
42 43
43 # Git settings 44 # Git settings
config/initializers/1_settings.rb
@@ -102,6 +102,10 @@ class Settings &lt; Settingslogic @@ -102,6 +102,10 @@ class Settings &lt; Settingslogic
102 git_host['admin_uri'] || 'git@localhost:gitolite-admin' 102 git_host['admin_uri'] || 'git@localhost:gitolite-admin'
103 end 103 end
104 104
  105 + def gitolite_admin_key
  106 + git_host['gitolite_admin_key'] || 'gitlab'
  107 + end
  108 +
105 def default_projects_limit 109 def default_projects_limit
106 app['default_projects_limit'] || 10 110 app['default_projects_limit'] || 10
107 end 111 end
doc/api/projects.md
@@ -112,6 +112,66 @@ Parameters: @@ -112,6 +112,66 @@ Parameters:
112 Will return created project with status `201 Created` on success, or `404 Not 112 Will return created project with status `201 Created` on success, or `404 Not
113 found` on fail. 113 found` on fail.
114 114
  115 +## Get project users
  116 +
  117 +Get users and access roles for existing project
  118 +
  119 +```
  120 +GET /projects/:id/users
  121 +```
  122 +
  123 +Parameters:
  124 +
  125 ++ `id` (required) - The ID or code name of a project
  126 +
  127 +Will return users and their access roles with status `200 OK` on success, or `404 Not found` on fail.
  128 +
  129 +## Add project users
  130 +
  131 +Add users to exiting project
  132 +
  133 +```
  134 +POST /projects/:id/users
  135 +```
  136 +
  137 +Parameters:
  138 +
  139 ++ `id` (required) - The ID or code name of a project
  140 ++ `user_ids` (required) - The ID list of users to add
  141 ++ `project_access` (required) - Project access level
  142 +
  143 +Will return status `201 Created` on success, or `404 Not found` on fail.
  144 +
  145 +## Update project users access level
  146 +
  147 +Update existing users to specified access level
  148 +
  149 +```
  150 +PUT /projects/:id/users
  151 +```
  152 +
  153 +Parameters:
  154 +
  155 ++ `id` (required) - The ID or code name of a project
  156 ++ `user_ids` (required) - The ID list of users to add
  157 ++ `project_access` (required) - Project access level
  158 +
  159 +Will return status `200 OK` on success, or `404 Not found` on fail.
  160 +
  161 +## Delete project users
  162 +
  163 +Delete users from exiting project
  164 +
  165 +```
  166 +DELETE /projects/:id/users
  167 +```
  168 +
  169 +Parameters:
  170 +
  171 ++ `id` (required) - The ID or code name of a project
  172 ++ `user_ids` (required) - The ID list of users to add
  173 +
  174 +Will return status `200 OK` on success, or `404 Not found` on fail.
115 175
116 ## Project repository branches 176 ## Project repository branches
117 177
doc/installation.md
@@ -113,17 +113,20 @@ Generate key: @@ -113,17 +113,20 @@ Generate key:
113 Clone GitLab's fork of the Gitolite source code: 113 Clone GitLab's fork of the Gitolite source code:
114 114
115 cd /home/git 115 cd /home/git
116 - sudo -H -u git git clone https://github.com/gitlabhq/gitolite.git /home/git/gitolite 116 + sudo -H -u git git clone -b gl-v304 https://github.com/gitlabhq/gitolite.git /home/git/gitolite
117 117
118 Setup: 118 Setup:
119 119
  120 + cd /home/git
  121 + sudo -u git -H mkdir bin
120 sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" >> /home/git/.profile' 122 sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" >> /home/git/.profile'
121 - sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; /home/git/gitolite/src/gl-system-install" 123 + sudo -u git sh -c 'gitolite/install -ln /home/git/bin'
  124 +
122 sudo cp /home/gitlab/.ssh/id_rsa.pub /home/git/gitlab.pub 125 sudo cp /home/gitlab/.ssh/id_rsa.pub /home/git/gitlab.pub
123 sudo chmod 0444 /home/git/gitlab.pub 126 sudo chmod 0444 /home/git/gitlab.pub
124 127
125 - sudo -u git -H sed -i 's/0077/0007/g' /home/git/share/gitolite/conf/example.gitolite.rc  
126 - sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; gl-setup -q /home/git/gitlab.pub" 128 + sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; gitolite setup -pk /home/git/gitlab.pub"
  129 + sudo -u git -H sed -i 's/0077/0007/g' /home/git/.gitolite.rc
127 130
128 Permissions: 131 Permissions:
129 132
@@ -189,8 +192,8 @@ and ensure you have followed all of the above steps carefully. @@ -189,8 +192,8 @@ and ensure you have followed all of the above steps carefully.
189 192
190 #### Setup GitLab hooks 193 #### Setup GitLab hooks
191 194
192 - sudo cp ./lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive  
193 - sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive 195 + sudo cp ./lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive
  196 + sudo chown git:git /home/git/.gitolite/hooks/common/post-receive
194 197
195 #### Check application status 198 #### Check application status
196 199
features/dashboard/dashboard.feature
@@ -15,4 +15,14 @@ Feature: Dashboard @@ -15,4 +15,14 @@ Feature: Dashboard
15 And I click "Create Merge Request" link 15 And I click "Create Merge Request" link
16 Then I see prefilled new Merge Request page 16 Then I see prefilled new Merge Request page
17 17
  18 + Scenario: I should see User joined Project event
  19 + Given user with name "John Doe" joined project "Shop"
  20 + When I visit dashboard page
  21 + Then I should see "John Doe joined project Shop" event
18 22
  23 + Scenario: I should see User left Project event
  24 + Given user with name "John Doe" joined project "Shop"
  25 + And user with name "John Doe" left project "Shop"
  26 + When I visit dashboard page
  27 + Then I should see "John Doe left project Shop" event
  28 +
features/projects/issues/issues.feature
@@ -64,3 +64,19 @@ Feature: Issues @@ -64,3 +64,19 @@ Feature: Issues
64 And I fill in issue search with "" 64 And I fill in issue search with ""
65 Then I should see "Release 0.4" in issues 65 Then I should see "Release 0.4" in issues
66 And I should see "Release 0.3" in issues 66 And I should see "Release 0.3" in issues
  67 +
  68 + @javascript
  69 + Scenario: I create Issue with pre-selected milestone
  70 + Given project "Shop" has milestone "v2.2"
  71 + And project "Shop" has milestone "v3.0"
  72 + And I visit project "Shop" issues page
  73 + When I select milestone "v3.0"
  74 + And I click link "New Issue"
  75 + Then I should see selected milestone with title "v3.0"
  76 +
  77 + @javascript
  78 + Scenario: I create Issue with pre-selected assignee
  79 + When I select first assignee from "Shop" project
  80 + And I click link "New Issue"
  81 + Then I should see first assignee from "Shop" as selected assignee
  82 +
features/step_definitions/dashboard_steps.rb
@@ -109,3 +109,28 @@ Given /^I have authored merge requests$/ do @@ -109,3 +109,28 @@ Given /^I have authored merge requests$/ do
109 :author => @user, 109 :author => @user,
110 :project => project2 110 :project => project2
111 end 111 end
  112 +
  113 +Given /^user with name "(.*?)" joined project "(.*?)"$/ do |user_name, project_name|
  114 + user = Factory.create(:user, {name: user_name})
  115 + project = Project.find_by_name project_name
  116 + Event.create(
  117 + project: project,
  118 + author_id: user.id,
  119 + action: Event::Joined
  120 + )
  121 +end
  122 +
  123 +Given /^user with name "(.*?)" left project "(.*?)"$/ do |user_name, project_name|
  124 + user = User.find_by_name user_name
  125 + project = Project.find_by_name project_name
  126 + Event.create(
  127 + project: project,
  128 + author_id: user.id,
  129 + action: Event::Left
  130 + )
  131 +end
  132 +
  133 +Then /^I should see "(.*?)" event$/ do |event_text|
  134 + page.should have_content(event_text)
  135 +end
  136 +
features/step_definitions/project/project_issues_steps.rb
@@ -55,3 +55,27 @@ Given /^I fill in issue search with &quot;(.*?)&quot;$/ do |arg1| @@ -55,3 +55,27 @@ Given /^I fill in issue search with &quot;(.*?)&quot;$/ do |arg1|
55 end 55 end
56 fill_in 'issue_search', with: arg1 56 fill_in 'issue_search', with: arg1
57 end 57 end
  58 +
  59 +When /^I select milestone "(.*?)"$/ do |milestone_title|
  60 + select milestone_title, from: "milestone_id"
  61 +end
  62 +
  63 +Then /^I should see selected milestone with title "(.*?)"$/ do |milestone_title|
  64 + issues_milestone_selector = "#issue_milestone_id_chzn/a"
  65 + wait_until{ page.has_content?("Details") }
  66 + page.find(issues_milestone_selector).should have_content(milestone_title)
  67 +end
  68 +
  69 +When /^I select first assignee from "(.*?)" project$/ do |project_name|
  70 + project = Project.find_by_name project_name
  71 + first_assignee = project.users.first
  72 + select first_assignee.name, from: "assignee_id"
  73 +end
  74 +
  75 +Then /^I should see first assignee from "(.*?)" as selected assignee$/ do |project_name|
  76 + issues_assignee_selector = "#issue_assignee_id_chzn/a"
  77 + wait_until{ page.has_content?("Details") }
  78 + project = Project.find_by_name project_name
  79 + assignee_name = project.users.first.name
  80 + page.find(issues_assignee_selector).should have_content(assignee_name)
  81 +end
features/step_definitions/project/project_team_steps.rb
@@ -22,8 +22,8 @@ end @@ -22,8 +22,8 @@ end
22 Given /^I select "(.*?)" as "(.*?)"$/ do |arg1, arg2| 22 Given /^I select "(.*?)" as "(.*?)"$/ do |arg1, arg2|
23 user = User.find_by_name(arg1) 23 user = User.find_by_name(arg1)
24 within "#new_team_member" do 24 within "#new_team_member" do
25 - select user.name, :from => "team_member_user_id"  
26 - select arg2, :from => "team_member_project_access" 25 + select user.name, :from => "user_ids"
  26 + select arg2, :from => "project_access"
27 end 27 end
28 click_button "Save" 28 click_button "Save"
29 end 29 end
lib/api/entities.rb
@@ -16,6 +16,11 @@ module Gitlab @@ -16,6 +16,11 @@ module Gitlab
16 expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at 16 expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at
17 end 17 end
18 18
  19 + class UsersProject < Grape::Entity
  20 + expose :user, using: Entities::UserBasic
  21 + expose :project_access
  22 + end
  23 +
19 class RepoObject < Grape::Entity 24 class RepoObject < Grape::Entity
20 expose :name, :commit 25 expose :name, :commit
21 end 26 end
lib/api/helpers.rb
@@ -21,5 +21,21 @@ module Gitlab @@ -21,5 +21,21 @@ module Gitlab
21 def authenticate! 21 def authenticate!
22 error!({'message' => '401 Unauthorized'}, 401) unless current_user 22 error!({'message' => '401 Unauthorized'}, 401) unless current_user
23 end 23 end
  24 +
  25 + def authorize! action, subject
  26 + unless abilities.allowed?(current_user, action, subject)
  27 + error!({'message' => '403 Forbidden'}, 403)
  28 + end
  29 + end
  30 +
  31 + private
  32 +
  33 + def abilities
  34 + @abilities ||= begin
  35 + abilities = Six.new
  36 + abilities << Ability
  37 + abilities
  38 + end
  39 + end
24 end 40 end
25 end 41 end
lib/api/issues.rb
@@ -79,6 +79,8 @@ module Gitlab @@ -79,6 +79,8 @@ module Gitlab
79 # PUT /projects/:id/issues/:issue_id 79 # PUT /projects/:id/issues/:issue_id
80 put ":id/issues/:issue_id" do 80 put ":id/issues/:issue_id" do
81 @issue = user_project.issues.find(params[:issue_id]) 81 @issue = user_project.issues.find(params[:issue_id])
  82 + authorize! :modify_issue, @issue
  83 +
82 parameters = { 84 parameters = {
83 title: (params[:title] || @issue.title), 85 title: (params[:title] || @issue.title),
84 description: (params[:description] || @issue.description), 86 description: (params[:description] || @issue.description),
lib/api/milestones.rb
@@ -61,6 +61,8 @@ module Gitlab @@ -61,6 +61,8 @@ module Gitlab
61 # Example Request: 61 # Example Request:
62 # PUT /projects/:id/milestones/:milestone_id 62 # PUT /projects/:id/milestones/:milestone_id
63 put ":id/milestones/:milestone_id" do 63 put ":id/milestones/:milestone_id" do
  64 + authorize! :admin_milestone, user_project
  65 +
64 @milestone = user_project.milestones.find(params[:milestone_id]) 66 @milestone = user_project.milestones.find(params[:milestone_id])
65 parameters = { 67 parameters = {
66 title: (params[:title] || @milestone.title), 68 title: (params[:title] || @milestone.title),
lib/api/projects.rb
@@ -54,6 +54,58 @@ module Gitlab @@ -54,6 +54,58 @@ module Gitlab
54 end 54 end
55 end 55 end
56 56
  57 + # Get project users
  58 + #
  59 + # Parameters:
  60 + # id (required) - The ID or code name of a project
  61 + # Example Request:
  62 + # GET /projects/:id/users
  63 + get ":id/users" do
  64 + @users_projects = paginate user_project.users_projects
  65 + present @users_projects, with: Entities::UsersProject
  66 + end
  67 +
  68 + # Add users to project with specified access level
  69 + #
  70 + # Parameters:
  71 + # id (required) - The ID or code name of a project
  72 + # user_ids (required) - The ID list of users to add
  73 + # project_access (required) - Project access level
  74 + # Example Request:
  75 + # POST /projects/:id/users
  76 + post ":id/users" do
  77 + authorize! :admin_project, user_project
  78 + user_project.add_users_ids_to_team(params[:user_ids].values, params[:project_access])
  79 + nil
  80 + end
  81 +
  82 + # Update users to specified access level
  83 + #
  84 + # Parameters:
  85 + # id (required) - The ID or code name of a project
  86 + # user_ids (required) - The ID list of users to add
  87 + # project_access (required) - New project access level to
  88 + # Example Request:
  89 + # PUT /projects/:id/add_users
  90 + put ":id/users" do
  91 + authorize! :admin_project, user_project
  92 + user_project.update_users_ids_to_role(params[:user_ids].values, params[:project_access])
  93 + nil
  94 + end
  95 +
  96 + # Delete project users
  97 + #
  98 + # Parameters:
  99 + # id (required) - The ID or code name of a project
  100 + # user_ids (required) - The ID list of users to delete
  101 + # Example Request:
  102 + # DELETE /projects/:id/users
  103 + delete ":id/users" do
  104 + authorize! :admin_project, user_project
  105 + user_project.delete_users_ids_from_team(params[:user_ids].values)
  106 + nil
  107 + end
  108 +
57 # Get a project repository branches 109 # Get a project repository branches
58 # 110 #
59 # Parameters: 111 # Parameters:
@@ -137,6 +189,8 @@ module Gitlab @@ -137,6 +189,8 @@ module Gitlab
137 # PUT /projects/:id/snippets/:snippet_id 189 # PUT /projects/:id/snippets/:snippet_id
138 put ":id/snippets/:snippet_id" do 190 put ":id/snippets/:snippet_id" do
139 @snippet = user_project.snippets.find(params[:snippet_id]) 191 @snippet = user_project.snippets.find(params[:snippet_id])
  192 + authorize! :modify_snippet, @snippet
  193 +
140 parameters = { 194 parameters = {
141 title: (params[:title] || @snippet.title), 195 title: (params[:title] || @snippet.title),
142 file_name: (params[:file_name] || @snippet.file_name), 196 file_name: (params[:file_name] || @snippet.file_name),
@@ -160,6 +214,8 @@ module Gitlab @@ -160,6 +214,8 @@ module Gitlab
160 # DELETE /projects/:id/snippets/:snippet_id 214 # DELETE /projects/:id/snippets/:snippet_id
161 delete ":id/snippets/:snippet_id" do 215 delete ":id/snippets/:snippet_id" do
162 @snippet = user_project.snippets.find(params[:snippet_id]) 216 @snippet = user_project.snippets.find(params[:snippet_id])
  217 + authorize! :modify_snippet, @snippet
  218 +
163 @snippet.destroy 219 @snippet.destroy
164 end 220 end
165 221
lib/gitlab/backend/gitolite.rb
1 -require 'gitolite'  
2 -require 'timeout'  
3 -require 'fileutils' 1 +require_relative 'gitolite_config'
4 2
5 -# TODO: refactor & cleanup  
6 module Gitlab 3 module Gitlab
7 class Gitolite 4 class Gitolite
8 class AccessDenied < StandardError; end 5 class AccessDenied < StandardError; end
9 - class InvalidKey < StandardError; end 6 +
  7 + def config
  8 + Gitlab::GitoliteConfig.new
  9 + end
10 10
11 def set_key key_id, key_content, projects 11 def set_key key_id, key_content, projects
12 - configure do |c|  
13 - c.update_keys(key_id, key_content)  
14 - c.update_projects(projects) 12 + config.apply do |config|
  13 + config.write_key(key_id, key_content)
  14 + config.update_projects(projects)
15 end 15 end
16 end 16 end
17 17
18 def remove_key key_id, projects 18 def remove_key key_id, projects
19 - configure do |c|  
20 - c.delete_key(key_id)  
21 - c.update_projects(projects) 19 + config.apply do |config|
  20 + config.rm_key(key_id)
  21 + config.update_projects(projects)
22 end 22 end
23 end 23 end
24 24
25 def update_repository project 25 def update_repository project
26 - configure do |c|  
27 - c.update_project(project.path, project)  
28 - end 26 + config.update_project!(project.path, project)
29 end 27 end
30 28
31 - alias_method :create_repository, :update_repository  
32 -  
33 def remove_repository project 29 def remove_repository project
34 - configure do |c|  
35 - c.destroy_project(project)  
36 - end 30 + config.destroy_project!(project)
37 end 31 end
38 32
39 def url_to_repo path 33 def url_to_repo path
40 Gitlab.config.ssh_path + "#{path}.git" 34 Gitlab.config.ssh_path + "#{path}.git"
41 end 35 end
42 36
43 - def initialize  
44 - # create tmp dir  
45 - @local_dir = File.join(Rails.root, 'tmp',"gitlabhq-gitolite-#{Time.now.to_i}")  
46 - end  
47 -  
48 def enable_automerge 37 def enable_automerge
49 - configure do |git|  
50 - git.admin_all_repo  
51 - end  
52 - end  
53 -  
54 - protected  
55 -  
56 - def destroy_project(project)  
57 - FileUtils.rm_rf(project.path_to_repo)  
58 -  
59 - ga_repo = ::Gitolite::GitoliteAdmin.new(File.join(@local_dir,'gitolite'))  
60 - conf = ga_repo.config  
61 - conf.rm_repo(project.path)  
62 - ga_repo.save  
63 - end  
64 -  
65 - #update or create  
66 - def update_keys(user, key)  
67 - File.open(File.join(@local_dir, 'gitolite/keydir',"#{user}.pub"), 'w') {|f| f.write(key.gsub(/\n/,'')) }  
68 - end  
69 -  
70 - def delete_key(user)  
71 - File.unlink(File.join(@local_dir, 'gitolite/keydir',"#{user}.pub"))  
72 - `cd #{File.join(@local_dir,'gitolite')} ; git rm keydir/#{user}.pub`  
73 - end  
74 -  
75 - # update or create  
76 - def update_project(repo_name, project)  
77 - ga_repo = ::Gitolite::GitoliteAdmin.new(File.join(@local_dir,'gitolite'))  
78 - conf = ga_repo.config  
79 - repo = update_project_config(project, conf)  
80 - conf.add_repo(repo, true)  
81 -  
82 - ga_repo.save  
83 - end  
84 -  
85 - # Updates many projects and uses project.path as the repo path  
86 - # An order of magnitude faster than update_project  
87 - def update_projects(projects)  
88 - ga_repo = ::Gitolite::GitoliteAdmin.new(File.join(@local_dir,'gitolite'))  
89 - conf = ga_repo.config  
90 -  
91 - projects.each do |project|  
92 - repo = update_project_config(project, conf)  
93 - conf.add_repo(repo, true)  
94 - end  
95 -  
96 - ga_repo.save  
97 - end  
98 -  
99 - def update_project_config(project, conf)  
100 - repo_name = project.path  
101 -  
102 - repo = if conf.has_repo?(repo_name)  
103 - conf.get_repo(repo_name)  
104 - else  
105 - ::Gitolite::Config::Repo.new(repo_name)  
106 - end  
107 -  
108 - name_readers = project.repository_readers  
109 - name_writers = project.repository_writers  
110 - name_masters = project.repository_masters  
111 -  
112 - pr_br = project.protected_branches.map(&:name).join("$ ")  
113 -  
114 - repo.clean_permissions  
115 -  
116 - # Deny access to protected branches for writers  
117 - unless name_writers.blank? || pr_br.blank?  
118 - repo.add_permission("-", pr_br.strip + "$ ", name_writers)  
119 - end  
120 -  
121 - # Add read permissions  
122 - repo.add_permission("R", "", name_readers) unless name_readers.blank?  
123 -  
124 - # Add write permissions  
125 - repo.add_permission("RW+", "", name_writers) unless name_writers.blank?  
126 - repo.add_permission("RW+", "", name_masters) unless name_masters.blank?  
127 -  
128 - repo  
129 - end  
130 -  
131 - def admin_all_repo  
132 - ga_repo = ::Gitolite::GitoliteAdmin.new(File.join(@local_dir,'gitolite'))  
133 - conf = ga_repo.config  
134 - owner_name = ""  
135 -  
136 - # Read gitolite-admin user  
137 - #  
138 - begin  
139 - repo = conf.get_repo("gitolite-admin")  
140 - owner_name = repo.permissions[0]["RW+"][""][0]  
141 - raise StandardError if owner_name.blank?  
142 - rescue => ex  
143 - puts "Can't determine gitolite-admin owner".red  
144 - raise StandardError  
145 - end  
146 -  
147 - # @ALL repos premission for gitolite owner  
148 - repo_name = "@all"  
149 - repo = if conf.has_repo?(repo_name)  
150 - conf.get_repo(repo_name)  
151 - else  
152 - ::Gitolite::Config::Repo.new(repo_name)  
153 - end  
154 -  
155 - repo.add_permission("RW+", "", owner_name)  
156 - conf.add_repo(repo, true)  
157 - ga_repo.save 38 + config.admin_all_repo!
158 end 39 end
159 40
160 - private  
161 -  
162 - def pull  
163 - # create tmp dir  
164 - @local_dir = File.join(Rails.root, 'tmp',"gitlabhq-gitolite-#{Time.now.to_i}")  
165 - Dir.mkdir @local_dir  
166 -  
167 - `git clone #{Gitlab.config.gitolite_admin_uri} #{@local_dir}/gitolite`  
168 - end  
169 -  
170 - def push  
171 - Dir.chdir(File.join(@local_dir, "gitolite"))  
172 - `git add -A`  
173 - `git commit -am "GitLab"`  
174 - `git push`  
175 - Dir.chdir(Rails.root)  
176 -  
177 - FileUtils.rm_rf(@local_dir)  
178 - end  
179 -  
180 - def configure  
181 - Timeout::timeout(30) do  
182 - File.open(File.join(Rails.root, 'tmp', "gitlabhq-gitolite.lock"), "w+") do |f|  
183 - begin  
184 - f.flock(File::LOCK_EX)  
185 - pull  
186 - yield(self)  
187 - push  
188 - ensure  
189 - f.flock(File::LOCK_UN)  
190 - end  
191 - end  
192 - end  
193 - rescue Exception => ex  
194 - if ex.message =~ /is not a valid SSH key string/  
195 - raise Gitolite::InvalidKey.new("ssh key is not valid")  
196 - else  
197 - Gitlab::Logger.error(ex.message)  
198 - raise Gitolite::AccessDenied.new("gitolite timeout")  
199 - end  
200 - end 41 + alias_method :create_repository, :update_repository
201 end 42 end
202 end 43 end
lib/gitlab/backend/gitolite_config.rb 0 → 100644
@@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
  1 +require 'gitolite'
  2 +require 'timeout'
  3 +require 'fileutils'
  4 +
  5 +module Gitlab
  6 + class GitoliteConfig
  7 + class PullError < StandardError; end
  8 + class PushError < StandardError; end
  9 +
  10 + attr_reader :config_tmp_dir, :ga_repo, :conf
  11 +
  12 + def config_tmp_dir
  13 + @config_tmp_dir ||= File.join(Rails.root, 'tmp',"gitlabhq-gitolite-#{Time.now.to_i}")
  14 + end
  15 +
  16 + def ga_repo
  17 + @ga_repo ||= ::Gitolite::GitoliteAdmin.new(File.join(config_tmp_dir,'gitolite'))
  18 + end
  19 +
  20 + def apply
  21 + Timeout::timeout(30) do
  22 + File.open(File.join(Rails.root, 'tmp', "gitlabhq-gitolite.lock"), "w+") do |f|
  23 + begin
  24 + # Set exclusive lock
  25 + # to prevent race condition
  26 + f.flock(File::LOCK_EX)
  27 +
  28 + # Pull gitolite-admin repo
  29 + # in tmp dir before do any changes
  30 + pull(config_tmp_dir)
  31 +
  32 + # Build ga_repo object and @conf
  33 + # to access gitolite-admin configuration
  34 + @conf = ga_repo.config
  35 +
  36 + # Do any changes
  37 + # in gitolite-admin
  38 + # config here
  39 + yield(self)
  40 +
  41 + # Save changes in
  42 + # gitolite-admin repo
  43 + # before pusht it
  44 + ga_repo.save
  45 +
  46 + # Push gitolite-admin repo
  47 + # to apply all changes
  48 + push(config_tmp_dir)
  49 +
  50 + # Remove tmp dir
  51 + # wiith gitolite-admin
  52 + FileUtils.rm_rf(config_tmp_dir)
  53 + ensure
  54 + # unlock so other task cann access
  55 + # gitolite configuration
  56 + f.flock(File::LOCK_UN)
  57 + end
  58 + end
  59 + end
  60 + rescue PullError => ex
  61 + Gitlab::Logger.error("Pull error -> " + ex.message)
  62 + raise Gitolite::AccessDenied, ex.message
  63 +
  64 + rescue PushError => ex
  65 + Gitlab::Logger.error("Push error -> " + " " + ex.message)
  66 + raise Gitolite::AccessDenied, ex.message
  67 +
  68 + rescue Exception => ex
  69 + Gitlab::Logger.error(ex.class.name + " " + ex.message)
  70 + raise Gitolite::AccessDenied.new("gitolite timeout")
  71 + end
  72 +
  73 + def destroy_project(project)
  74 + FileUtils.rm_rf(project.path_to_repo)
  75 + conf.rm_repo(project.path)
  76 + end
  77 +
  78 + def destroy_project!(project)
  79 + apply do |config|
  80 + config.destroy_project(project)
  81 + end
  82 + end
  83 +
  84 + def write_key(id, key)
  85 + File.open(File.join(config_tmp_dir, 'gitolite/keydir',"#{id}.pub"), 'w') do |f|
  86 + f.write(key.gsub(/\n/,''))
  87 + end
  88 + end
  89 +
  90 + def rm_key(user)
  91 + File.unlink(File.join(config_tmp_dir, 'gitolite/keydir',"#{user}.pub"))
  92 + `cd #{File.join(config_tmp_dir,'gitolite')} ; git rm keydir/#{user}.pub`
  93 + end
  94 +
  95 + # update or create
  96 + def update_project(repo_name, project)
  97 + repo = update_project_config(project, conf)
  98 + conf.add_repo(repo, true)
  99 + end
  100 +
  101 + def update_project!(repo_name, project)
  102 + apply do |config|
  103 + config.update_project(repo_name, project)
  104 + end
  105 + end
  106 +
  107 + # Updates many projects and uses project.path as the repo path
  108 + # An order of magnitude faster than update_project
  109 + def update_projects(projects)
  110 + projects.each do |project|
  111 + repo = update_project_config(project, conf)
  112 + conf.add_repo(repo, true)
  113 + end
  114 + end
  115 +
  116 + def update_project_config(project, conf)
  117 + repo_name = project.path
  118 +
  119 + repo = if conf.has_repo?(repo_name)
  120 + conf.get_repo(repo_name)
  121 + else
  122 + ::Gitolite::Config::Repo.new(repo_name)
  123 + end
  124 +
  125 + name_readers = project.repository_readers
  126 + name_writers = project.repository_writers
  127 + name_masters = project.repository_masters
  128 +
  129 + pr_br = project.protected_branches.map(&:name).join("$ ")
  130 +
  131 + repo.clean_permissions
  132 +
  133 + # Deny access to protected branches for writers
  134 + unless name_writers.blank? || pr_br.blank?
  135 + repo.add_permission("-", pr_br.strip + "$ ", name_writers)
  136 + end
  137 +
  138 + # Add read permissions
  139 + repo.add_permission("R", "", name_readers) unless name_readers.blank?
  140 +
  141 + # Add write permissions
  142 + repo.add_permission("RW+", "", name_writers) unless name_writers.blank?
  143 + repo.add_permission("RW+", "", name_masters) unless name_masters.blank?
  144 +
  145 + repo
  146 + end
  147 +
  148 + # Enable access to all repos for gitolite admin.
  149 + # We use it for accept merge request feature
  150 + def admin_all_repo
  151 + owner_name = Gitlab.settings.gitolite_admin_key
  152 +
  153 + # @ALL repos premission for gitolite owner
  154 + repo_name = "@all"
  155 + repo = if conf.has_repo?(repo_name)
  156 + conf.get_repo(repo_name)
  157 + else
  158 + ::Gitolite::Config::Repo.new(repo_name)
  159 + end
  160 +
  161 + repo.add_permission("RW+", "", owner_name)
  162 + conf.add_repo(repo, true)
  163 + end
  164 +
  165 + def admin_all_repo!
  166 + apply { |config| config.admin_all_repo }
  167 + end
  168 +
  169 + private
  170 +
  171 + def pull tmp_dir
  172 + Dir.mkdir tmp_dir
  173 + `git clone #{Gitlab.config.gitolite_admin_uri} #{tmp_dir}/gitolite`
  174 +
  175 + unless File.exists?(File.join(tmp_dir, 'gitolite', 'conf', 'gitolite.conf'))
  176 + raise PullError, "unable to clone gitolite-admin repo"
  177 + end
  178 + end
  179 +
  180 + def push tmp_dir
  181 + Dir.chdir(File.join(tmp_dir, "gitolite"))
  182 + system('git add -A')
  183 + system('git commit -am "GitLab"')
  184 + if system('git push')
  185 + Dir.chdir(Rails.root)
  186 + else
  187 + raise PushError, "unable to push gitolite-admin repo"
  188 + end
  189 + end
  190 + end
  191 +end
  192 +
lib/gitlab/markdown.rb
@@ -47,7 +47,9 @@ module Gitlab @@ -47,7 +47,9 @@ module Gitlab
47 # Note: reference links will only be generated if @project is set 47 # Note: reference links will only be generated if @project is set
48 def gfm(text, html_options = {}) 48 def gfm(text, html_options = {})
49 return text if text.nil? 49 return text if text.nil?
50 - return text if @project.nil? 50 +
  51 + # prevents the string supplied through the _text_ argument to be altered
  52 + text = text.dup
51 53
52 @html_options = html_options 54 @html_options = html_options
53 55
@@ -78,9 +80,12 @@ module Gitlab @@ -78,9 +80,12 @@ module Gitlab
78 # 80 #
79 # text - Text to parse 81 # text - Text to parse
80 # 82 #
  83 + # Note: reference links will only be generated if @project is set
  84 + #
81 # Returns parsed text 85 # Returns parsed text
82 def parse(text) 86 def parse(text)
83 - text = text.gsub(REFERENCE_PATTERN) do |match| 87 + # parse reference links
  88 + text.gsub!(REFERENCE_PATTERN) do |match|
84 prefix = $1 || '' 89 prefix = $1 || ''
85 reference = $2 90 reference = $2
86 identifier = $3 || $4 || $5 91 identifier = $3 || $4 || $5
@@ -91,9 +96,10 @@ module Gitlab @@ -91,9 +96,10 @@ module Gitlab
91 else 96 else
92 match 97 match
93 end 98 end
94 - end 99 + end if @project
95 100
96 - text = text.gsub(EMOJI_PATTERN) do |match| 101 + # parse emoji
  102 + text.gsub!(EMOJI_PATTERN) do |match|
97 if valid_emoji?($2) 103 if valid_emoji?($2)
98 image_tag("emoji/#{$2}.png", size: "20x20", class: 'emoji', title: $1, alt: $1) 104 image_tag("emoji/#{$2}.png", size: "20x20", class: 'emoji', title: $1, alt: $1)
99 else 105 else
lib/gitlab/merge.rb
@@ -21,8 +21,7 @@ module Gitlab @@ -21,8 +21,7 @@ module Gitlab
21 if output =~ /CONFLICT/ 21 if output =~ /CONFLICT/
22 false 22 false
23 else 23 else
24 - repo.git.push({}, "origin", merge_request.target_branch)  
25 - true 24 + !!repo.git.push({}, "origin", merge_request.target_branch)
26 end 25 end
27 end 26 end
28 end 27 end
lib/tasks/bulk_import.rake
1 -IMPORT_DIRECTORY = 'import_projects'  
2 -  
3 -desc "Imports existing Git repos into new projects from the import_projects folder"  
4 -task :import_projects, [:email] => :environment do |t, args|  
5 - REPOSITORY_DIRECTORY = Gitlab.config.git_base_path  
6 1
  2 +desc "Imports existing Git repos from a directory into new projects in git_base_path"
  3 +task :import_projects, [:directory,:email] => :environment do |t, args|
7 user_email = args.email 4 user_email = args.email
8 - repos_to_import = Dir.glob("#{IMPORT_DIRECTORY}/*")  
9 - 5 + import_directory = args.directory
  6 + repos_to_import = Dir.glob("#{import_directory}/*")
  7 + git_base_path = Gitlab.config.git_base_path
10 puts "Found #{repos_to_import.length} repos to import" 8 puts "Found #{repos_to_import.length} repos to import"
11 9
12 imported_count = 0 10 imported_count = 0
@@ -14,11 +12,9 @@ task :import_projects, [:email] =&gt; :environment do |t, args| @@ -14,11 +12,9 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
14 failed_count = 0 12 failed_count = 0
15 repos_to_import.each do |repo_path| 13 repos_to_import.each do |repo_path|
16 repo_name = File.basename repo_path 14 repo_name = File.basename repo_path
17 - repo_full_path = File.join(Rails.root, repo_path)  
18 15
19 puts " Processing #{repo_name}" 16 puts " Processing #{repo_name}"
20 -  
21 - clone_path = "#{REPOSITORY_DIRECTORY}/#{repo_name}.git" 17 + clone_path = "#{git_base_path}#{repo_name}.git"
22 18
23 if Dir.exists? clone_path 19 if Dir.exists? clone_path
24 if Project.find_by_code(repo_name) 20 if Project.find_by_code(repo_name)
@@ -30,7 +26,7 @@ task :import_projects, [:email] =&gt; :environment do |t, args| @@ -30,7 +26,7 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
30 end 26 end
31 else 27 else
32 # Clone the repo 28 # Clone the repo
33 - unless clone_bare_repo_as_git(repo_full_path, clone_path) 29 + unless clone_bare_repo_as_git(repo_path, clone_path)
34 failed_count += 1 30 failed_count += 1
35 next 31 next
36 end 32 end
@@ -48,14 +44,17 @@ task :import_projects, [:email] =&gt; :environment do |t, args| @@ -48,14 +44,17 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
48 puts "Finished importing #{imported_count} projects (skipped #{skipped_count}, failed #{failed_count})." 44 puts "Finished importing #{imported_count} projects (skipped #{skipped_count}, failed #{failed_count})."
49 end 45 end
50 46
51 -# Clones a repo as bare git repo using the git user 47 +# Clones a repo as bare git repo using the git_user
52 def clone_bare_repo_as_git(existing_path, new_path) 48 def clone_bare_repo_as_git(existing_path, new_path)
  49 + git_user = Gitlab.config.ssh_user
53 begin 50 begin
54 - sh "sudo -u git -i git clone --bare '#{existing_path}' #{new_path}" 51 + sh "sudo -u #{git_user} -i git clone --bare '#{existing_path}' #{new_path}"
55 true 52 true
56 - rescue 53 + rescue Exception=> msg
57 puts " ERROR: Faild to clone #{existing_path} to #{new_path}" 54 puts " ERROR: Faild to clone #{existing_path} to #{new_path}"
58 - false 55 + puts " Make sure #{git_user} can reach #{existing_path}"
  56 + puts " Exception-MSG: #{msg}"
  57 + false
59 end 58 end
60 end 59 end
61 60
spec/helpers/gitlab_markdown_helper_spec.rb
@@ -247,6 +247,11 @@ describe GitlabMarkdownHelper do @@ -247,6 +247,11 @@ describe GitlabMarkdownHelper do
247 it "ignores invalid emoji" do 247 it "ignores invalid emoji" do
248 gfm(":invalid-emoji:").should_not match(/<img/) 248 gfm(":invalid-emoji:").should_not match(/<img/)
249 end 249 end
  250 +
  251 + it "should work independet of reference links (i.e. without @project being set)" do
  252 + @project = nil
  253 + gfm(":+1:").should match(/<img/)
  254 + end
250 end 255 end
251 end 256 end
252 257
spec/helpers/tree_helper_spec.rb 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +require 'spec_helper'
  2 +
  3 +describe TreeHelper do
  4 + describe '#markup?' do
  5 + %w(mdown md markdown textile rdoc org creole mediawiki rst asciidoc pod).each do |type|
  6 + it "returns true for #{type} files" do
  7 + markup?("README.#{type}").should be_true
  8 + end
  9 + end
  10 +
  11 + it "returns false when given a non-markup filename" do
  12 + markup?('README.rb').should_not be_true
  13 + end
  14 + end
  15 +end
spec/lib/gitolite_config_spec.rb 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Gitlab::GitoliteConfig do
  4 + let(:gitolite) { Gitlab::GitoliteConfig.new }
  5 +
  6 + it { should respond_to :write_key }
  7 + it { should respond_to :rm_key }
  8 + it { should respond_to :update_project }
  9 + it { should respond_to :update_project! }
  10 + it { should respond_to :update_projects }
  11 + it { should respond_to :destroy_project }
  12 + it { should respond_to :destroy_project! }
  13 + it { should respond_to :apply }
  14 + it { should respond_to :admin_all_repo }
  15 + it { should respond_to :admin_all_repo! }
  16 +end
spec/lib/gitolite_spec.rb 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Gitlab::Gitolite do
  4 + let(:project) { double('Project', path: 'diaspora') }
  5 + let(:gitolite_config) { double('Gitlab::GitoliteConfig') }
  6 + let(:gitolite) { Gitlab::Gitolite.new }
  7 +
  8 + before do
  9 + gitolite.stub(config: gitolite_config)
  10 + end
  11 +
  12 + it { should respond_to :set_key }
  13 + it { should respond_to :remove_key }
  14 +
  15 + it { should respond_to :update_repository }
  16 + it { should respond_to :create_repository }
  17 + it { should respond_to :remove_repository }
  18 +
  19 + it { gitolite.url_to_repo('diaspora').should == Gitlab.config.ssh_path + "diaspora.git" }
  20 +
  21 + it "should call config update" do
  22 + gitolite_config.should_receive(:update_project!)
  23 + gitolite.update_repository project
  24 + end
  25 +end
spec/models/event_spec.rb
@@ -49,4 +49,26 @@ describe Event do @@ -49,4 +49,26 @@ describe Event do
49 it { @event.branch_name.should == "master" } 49 it { @event.branch_name.should == "master" }
50 it { @event.author.should == @user } 50 it { @event.author.should == @user }
51 end 51 end
  52 +
  53 + describe "Joined project team" do
  54 + let(:project) {Factory.create :project}
  55 + let(:new_user) {Factory.create :user}
  56 + it "should create event" do
  57 + UsersProject.observers.enable :users_project_observer
  58 + expect{
  59 + UsersProject.bulk_import(project, [new_user.id], UsersProject::DEVELOPER)
  60 + }.to change{Event.count}.by(1)
  61 + end
  62 + end
  63 + describe "Left project team" do
  64 + let(:project) {Factory.create :project}
  65 + let(:new_user) {Factory.create :user}
  66 + it "should create event" do
  67 + UsersProject.bulk_import(project, [new_user.id], UsersProject::DEVELOPER)
  68 + UsersProject.observers.enable :users_project_observer
  69 + expect{
  70 + UsersProject.bulk_delete(project, [new_user.id])
  71 + }.to change{Event.count}.by(1)
  72 + end
  73 + end
52 end 74 end
spec/observers/users_project_observer_spec.rb
@@ -23,6 +23,17 @@ describe UsersProjectObserver do @@ -23,6 +23,17 @@ describe UsersProjectObserver do
23 Notify.should_receive(:project_access_granted_email).with(users_project.id).and_return(double(deliver: true)) 23 Notify.should_receive(:project_access_granted_email).with(users_project.id).and_return(double(deliver: true))
24 subject.after_commit(users_project) 24 subject.after_commit(users_project)
25 end 25 end
  26 + it "should create new event" do
  27 + Event.should_receive(:create).with(
  28 + project_id: users_project.project.id,
  29 + action: Event::Joined,
  30 + author_id: users_project.user.id
  31 + )
  32 + subject.after_create(users_project)
  33 + end
  34 + end
  35 +
  36 + describe "#after_update" do
26 it "should called when UsersProject updated" do 37 it "should called when UsersProject updated" do
27 subject.should_receive(:after_commit).once 38 subject.should_receive(:after_commit).once
28 UsersProject.observers.enable :users_project_observer do 39 UsersProject.observers.enable :users_project_observer do
@@ -40,4 +51,23 @@ describe UsersProjectObserver do @@ -40,4 +51,23 @@ describe UsersProjectObserver do
40 end 51 end
41 end 52 end
42 end 53 end
  54 + describe "#after_destroy" do
  55 + it "should called when UsersProject destroyed" do
  56 + subject.should_receive(:after_destroy)
  57 + UsersProject.observers.enable :users_project_observer do
  58 + UsersProject.bulk_delete(
  59 + users_project.project,
  60 + [users_project.user.id]
  61 + )
  62 + end
  63 + end
  64 + it "should create new event" do
  65 + Event.should_receive(:create).with(
  66 + project_id: users_project.project.id,
  67 + action: Event::Left,
  68 + author_id: users_project.user.id
  69 + )
  70 + subject.after_destroy(users_project)
  71 + end
  72 + end
43 end 73 end
spec/requests/api/projects_spec.rb
@@ -4,8 +4,12 @@ describe Gitlab::API do @@ -4,8 +4,12 @@ describe Gitlab::API do
4 include ApiHelpers 4 include ApiHelpers
5 5
6 let(:user) { Factory :user } 6 let(:user) { Factory :user }
  7 + let(:user2) { Factory.create(:user) }
  8 + let(:user3) { Factory.create(:user) }
7 let!(:project) { Factory :project, owner: user } 9 let!(:project) { Factory :project, owner: user }
8 let!(:snippet) { Factory :snippet, author: user, project: project, title: 'example' } 10 let!(:snippet) { Factory :snippet, author: user, project: project, title: 'example' }
  11 + let!(:users_project) { Factory :users_project, user: user, project: project, project_access: UsersProject::MASTER }
  12 + let!(:users_project2) { Factory :users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER }
9 before { project.add_access(user, :read) } 13 before { project.add_access(user, :read) }
10 14
11 describe "GET /projects" do 15 describe "GET /projects" do
@@ -104,6 +108,45 @@ describe Gitlab::API do @@ -104,6 +108,45 @@ describe Gitlab::API do
104 end 108 end
105 end 109 end
106 110
  111 + describe "GET /projects/:id/users" do
  112 + it "should return project users" do
  113 + get api("/projects/#{project.code}/users", user)
  114 +
  115 + response.status.should == 200
  116 +
  117 + json_response.should be_an Array
  118 + json_response.count.should == 2
  119 + json_response.first['user']['id'].should == user.id
  120 + end
  121 + end
  122 +
  123 + describe "POST /projects/:id/users" do
  124 + it "should add users to project" do
  125 + expect {
  126 + post api("/projects/#{project.code}/users", user),
  127 + user_ids: {"0" => user2.id}, project_access: UsersProject::DEVELOPER
  128 + }.to change {project.users_projects.where(:project_access => UsersProject::DEVELOPER).count}.by(1)
  129 + end
  130 + end
  131 +
  132 + describe "PUT /projects/:id/users" do
  133 + it "should update users to new access role" do
  134 + expect {
  135 + put api("/projects/#{project.code}/users", user),
  136 + user_ids: {"0" => user3.id}, project_access: UsersProject::MASTER
  137 + }.to change {project.users_projects.where(:project_access => UsersProject::MASTER).count}.by(1)
  138 + end
  139 + end
  140 +
  141 + describe "DELETE /projects/:id/users" do
  142 + it "should delete users from project" do
  143 + expect {
  144 + delete api("/projects/#{project.code}/users", user),
  145 + user_ids: {"0" => user3.id}
  146 + }.to change {project.users_projects.count}.by(-1)
  147 + end
  148 + end
  149 +
107 describe "GET /projects/:id/repository/tags" do 150 describe "GET /projects/:id/repository/tags" do
108 it "should return an array of project tags" do 151 it "should return an array of project tags" do
109 get api("/projects/#{project.code}/repository/tags", user) 152 get api("/projects/#{project.code}/repository/tags", user)
spec/requests/projects_spec.rb
@@ -3,6 +3,16 @@ require &#39;spec_helper&#39; @@ -3,6 +3,16 @@ require &#39;spec_helper&#39;
3 describe "Projects" do 3 describe "Projects" do
4 before { login_as :user } 4 before { login_as :user }
5 5
  6 + describe 'GET /project/new' do
  7 + it "should work autocomplete", :js => true do
  8 + visit new_project_path
  9 +
  10 + fill_in 'project_name', with: 'Awesome'
  11 + find("#project_path").value.should == 'awesome'
  12 + find("#project_code").value.should == 'awesome'
  13 + end
  14 + end
  15 +
6 describe "GET /projects/show" do 16 describe "GET /projects/show" do
7 before do 17 before do
8 @project = Factory :project, owner: @user 18 @project = Factory :project, owner: @user
spec/support/gitolite_stub.rb
@@ -17,7 +17,7 @@ module GitoliteStub @@ -17,7 +17,7 @@ module GitoliteStub
17 ) 17 )
18 18
19 gitolite_admin = double( 19 gitolite_admin = double(
20 - 'Gitolite::GitoliteAdmin', 20 + 'Gitolite::GitoliteAdmin',
21 config: gitolite_config, 21 config: gitolite_config,
22 save: true, 22 save: true,
23 ) 23 )
@@ -27,9 +27,21 @@ module GitoliteStub @@ -27,9 +27,21 @@ module GitoliteStub
27 end 27 end
28 28
29 def stub_gitlab_gitolite 29 def stub_gitlab_gitolite
30 - gitlab_gitolite = Gitlab::Gitolite.new  
31 - Gitlab::Gitolite.stub(new: gitlab_gitolite)  
32 - gitlab_gitolite.stub(configure: ->() { yield(self) })  
33 - gitlab_gitolite.stub(update_keys: true) 30 + gitolite_config = double('Gitlab::GitoliteConfig')
  31 + gitolite_config.stub(
  32 + apply: ->() { yield(self) },
  33 + write_key: true,
  34 + rm_key: true,
  35 + update_projects: true,
  36 + update_project: true,
  37 + update_project!: true,
  38 + destroy_project: true,
  39 + destroy_project!: true,
  40 + admin_all_repo: true,
  41 + admin_all_repo!: true,
  42 +
  43 + )
  44 +
  45 + Gitlab::GitoliteConfig.stub(new: gitolite_config)
34 end 46 end
35 end 47 end