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
Gemfile
... ... @@ -44,7 +44,8 @@ gem "ffaker"
44 44 gem "seed-fu"
45 45  
46 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 50 # Servers
50 51 gem "thin"
... ...
Gemfile.lock
... ... @@ -108,7 +108,7 @@ GEM
108 108 bcrypt-ruby (3.0.1)
109 109 blankslate (2.1.2.4)
110 110 bootstrap-sass (2.0.4.0)
111   - builder (3.0.0)
  111 + builder (3.0.2)
112 112 capybara (1.1.2)
113 113 mime-types (>= 1.16)
114 114 nokogiri (>= 1.3.3)
... ... @@ -125,7 +125,7 @@ GEM
125 125 charlock_holmes (0.6.8)
126 126 childprocess (0.3.2)
127 127 ffi (~> 1.0.6)
128   - chosen-rails (0.9.8)
  128 + chosen-rails (0.9.8.3)
129 129 railties (~> 3.0)
130 130 thor (~> 0.14)
131 131 coderay (1.0.6)
... ... @@ -178,6 +178,7 @@ GEM
178 178 gherkin (2.11.0)
179 179 json (>= 1.4.6)
180 180 git (1.2.5)
  181 + github-markup (0.7.4)
181 182 gitlab_meta (2.9)
182 183 grape (0.2.1)
183 184 hashie (~> 1.2)
... ... @@ -397,6 +398,7 @@ DEPENDENCIES
397 398 ffaker
398 399 foreman
399 400 git
  401 + github-markup (~> 0.7.4)
400 402 gitlab_meta (= 2.9)
401 403 gitolite!
402 404 grack!
... ...
app/assets/javascripts/issues.js
... ... @@ -5,7 +5,7 @@ function switchToNewIssue(form){
5 5 $('select#issue_milestone_id').chosen();
6 6 $("#new_issue_dialog").show("fade", { direction: "right" }, 150);
7 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 16 $('select#issue_milestone_id').chosen();
17 17 $("#edit_issue_dialog").show("fade", { direction: "right" }, 150);
18 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 80 $(this).closest("form").submit();
81 81 });
82 82  
  83 + $("#new_issue_link").click(function(){
  84 + updateNewIssueURL();
  85 + });
  86 +
83 87 $('body').on('ajax:success', '.close_issue, .reopen_issue, #new_issue', function(){
84 88 var t = $(this),
85 89 totalIssues,
... ... @@ -126,3 +130,20 @@ function issuesCheckChanged() {
126 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   -$(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 @@
  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 25 $(this).closest('li').fadeOut(); });
26 26  
27 27 $(".note-form-holder").live("ajax:before", function(){
28   - $(".submit_note").attr("disabled", "disabled");
  28 + $(".submit_note").disable()
29 29 })
30 30  
31 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 37 $(".note-text").live("focus", function(){
38 38 $(this).css("height", "80px");
... ... @@ -177,6 +177,6 @@ var PerLineNotes = {
177 177 form.show();
178 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 8 $('.save-project-loader').show()
9 9  
10 10 $('form #project_default_branch').chosen()
11   - disableButtonIfEmtpyField '#project_name', '.project-submit'
  11 + disableButtonIfEmptyField '#project_name', '.project-submit'
12 12  
13 13 # Git clone panel switcher
14 14 $ ->
... ...
app/assets/stylesheets/common.scss
... ... @@ -179,6 +179,14 @@ span.update-author {
179 179 &.merged {
180 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 192 form {
... ...
app/controllers/application_controller.rb
... ... @@ -11,15 +11,11 @@ class ApplicationController < ActionController::Base
11 11 helper_method :abilities, :can?
12 12  
13 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 15 end
20 16  
21 17 rescue_from Encoding::CompatibilityError do |exception|
22   - render "errors/encoding", layout: "error", status: 404
  18 + render "errors/encoding", layout: "error", status: 500
23 19 end
24 20  
25 21 rescue_from ActiveRecord::RecordNotFound do |exception|
... ...
app/controllers/issues_controller.rb
... ... @@ -37,7 +37,7 @@ class IssuesController < ApplicationController
37 37 end
38 38  
39 39 def new
40   - @issue = @project.issues.new
  40 + @issue = @project.issues.new(params[:issue])
41 41 respond_with(@issue)
42 42 end
43 43  
... ...
app/controllers/refs_controller.rb
  1 +require 'github/markup'
  2 +
1 3 class RefsController < ApplicationController
2 4 include Gitlab::Encode
3 5 before_filter :project
... ...
app/controllers/team_members_controller.rb
... ... @@ -17,13 +17,12 @@ class TeamMembersController &lt; ApplicationController
17 17 end
18 18  
19 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 26 end
28 27  
29 28 def update
... ...
app/decorators/event_decorator.rb
... ... @@ -8,7 +8,9 @@ class EventDecorator &lt; ApplicationDecorator
8 8 "#{self.author_name} #{self.action_name} MR ##{self.target_id}:" + self.merge_request_title
9 9 elsif self.push?
10 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 15 end
14 16 end
... ...
app/helpers/gitlab_markdown_helper.rb
... ... @@ -27,7 +27,7 @@ module GitlabMarkdownHelper
27 27 filter_html: true,
28 28 with_toc_data: true,
29 29 hard_wrap: true)
30   - @markdown ||= Redcarpet::Markdown.new(gitlab_renderer,
  30 + @markdown = Redcarpet::Markdown.new(gitlab_renderer,
31 31 # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
32 32 no_intra_emphasis: true,
33 33 tables: true,
... ...
app/helpers/projects_helper.rb 0 → 100644
... ... @@ -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 24 content.name
25 25 end
26 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 37 end
... ...
app/models/event.rb
... ... @@ -10,6 +10,8 @@ class Event &lt; ActiveRecord::Base
10 10 Pushed = 5
11 11 Commented = 6
12 12 Merged = 7
  13 + Joined = 8 # User joined project
  14 + Left = 9 # User left project
13 15  
14 16 belongs_to :project
15 17 belongs_to :target, polymorphic: true
... ... @@ -37,7 +39,7 @@ class Event &lt; ActiveRecord::Base
37 39 # - new issue
38 40 # - merge request
39 41 def allowed?
40   - push? || issue? || merge_request?
  42 + push? || issue? || merge_request? || membership_changed?
41 43 end
42 44  
43 45 def push?
... ... @@ -84,6 +86,18 @@ class Event &lt; ActiveRecord::Base
84 86 [Closed, Reopened].include?(action)
85 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 101 def issue
88 102 target if target_type == "Issue"
89 103 end
... ... @@ -101,6 +115,10 @@ class Event &lt; ActiveRecord::Base
101 115 "closed"
102 116 elsif merged?
103 117 "merged"
  118 + elsif joined?
  119 + 'joined'
  120 + elsif left?
  121 + 'left'
104 122 else
105 123 "opened"
106 124 end
... ...
app/models/merge_request.rb
... ... @@ -162,7 +162,7 @@ class MergeRequest &lt; ActiveRecord::Base
162 162 end
163 163  
164 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 166 self.merge!(current_user.id)
167 167 true
168 168 end
... ...
app/models/users_project.rb
... ... @@ -20,6 +20,23 @@ class UsersProject &lt; ActiveRecord::Base
20 20  
21 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 40 def self.bulk_import(project, user_ids, project_access)
24 41 UsersProject.transaction do
25 42 user_ids.each do |user_id|
... ...
app/observers/users_project_observer.rb
... ... @@ -3,4 +3,20 @@ class UsersProjectObserver &lt; ActiveRecord::Observer
3 3 return if users_project.destroyed?
4 4 Notify.project_access_granted_email(users_project.id).deliver
5 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 22 end
... ...
app/roles/push_event.rb
... ... @@ -90,6 +90,8 @@ module PushEvent
90 90  
91 91 def push_with_commits?
92 92 md_ref? && commits.any? && parent_commit && last_commit
  93 + rescue Grit::NoSuchPathError
  94 + false
93 95 end
94 96  
95 97 def last_push_to_non_root?
... ...
app/roles/team.rb
... ... @@ -36,4 +36,17 @@ module Team
36 36 UsersProject.bulk_import(self, users_ids, access_role)
37 37 self.update_repository
38 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 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   -%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 11 .event_feed
12 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 @@
  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 20 %li milestones
21 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 32 %h3 Differences from traditional Markdown
24 33  
25 34 %h4 Newlines
... ... @@ -62,6 +71,29 @@
62 71 %p becomes
63 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 97 %h4 Special GitLab references
66 98  
67 99 %p
... ... @@ -93,12 +125,5 @@
93 125 %p For example in your #{link_to @project.name, project_path(@project)} project, writing:
94 126 %pre= "This is related to ##{issue.id}. @#{current_user.name} is working on solving it."
95 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 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 6 .right
7 7 .span5
8 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 10 %i.icon-plus
11 11 New Issue
12 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 3 = link_to "Add new", new_key_path, class: "btn right"
4 4  
5 5 %hr
6   -%p.slead
  6 +%p.slead
7 7 SSH key allows you to establish a secure connection between your computer and GitLab
8 8  
9 9  
... ... @@ -15,7 +15,7 @@
15 15 %th
16 16 - @keys.each do |key|
17 17 = render(partial: 'show', locals: {key: key})
18   - - if @keys.blank?
  18 + - if @keys.blank?
19 19 %tr
20 20 %td{colspan: 3}
21 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 34 source: #{raw search_autocomplete_source},
35 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 60  
61 61 :javascript
62 62 $(function(){
63   - disableButtonIfEmtpyField("#merge_request_title", ".save-btn");
  63 + disableButtonIfEmptyField("#merge_request_title", ".save-btn");
64 64 $('select#merge_request_assignee_id').chosen();
65 65 $('select#merge_request_source_branch').chosen();
66 66 $('select#merge_request_target_branch').chosen();
... ...
app/views/milestones/_form.html.haml
... ... @@ -41,7 +41,7 @@
41 41  
42 42 :javascript
43 43 $(function() {
44   - disableButtonIfEmtpyField("#milestone_title", ".save-btn");
  44 + disableButtonIfEmptyField("#milestone_title", ".save-btn");
45 45 $( ".datepicker" ).datepicker({
46 46 dateFormat: "yy-mm-dd",
47 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 13 :javascript
... ...
app/views/refs/_tree.html.haml
... ... @@ -43,11 +43,7 @@
43 43 %i.icon-file
44 44 = content.name
45 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 48 :javascript
53 49 $(function(){
... ...
app/views/refs/_tree_file.html.haml
... ... @@ -9,10 +9,9 @@
9 9 = link_to "history", project_commits_path(@project, path: params[:path], ref: @ref), class: "btn very_small"
10 10 = link_to "blame", blame_file_project_ref_path(@project, @ref, path: params[:path]), class: "btn very_small"
11 11 - if file.text?
12   - - if name =~ /\.(md|markdown)$/i
  12 + - if markup?(name)
13 13 .file_content.wiki
14   - = preserve do
15   - = markdown(file.data)
  14 + = raw GitHub::Markup.render(name, file.data)
16 15 - else
17 16 .file_content.code
18 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 3 %hr
3 4 = form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f|
4 5 -if @team_member.errors.any?
... ... @@ -7,27 +8,23 @@
7 8 - @team_member.errors.full_messages.each do |msg|
8 9 %li= msg
9 10  
  11 + %h6 1. Choose people you want in the team
10 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 18 .clearfix
16 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 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 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 2 - allow_admin = can? current_user, :admin_project, @project
3 3 %tr{id: dom_id(member), class: "team_member_row user_#{user.id}"}
4 4 %td
5   - .right
6   - - if @project.owner == user
7   - %span.label Project Owner
8   - - if user.blocked
9   - %span.label Blocked
10   -
11 5 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do
12 6 = image_tag gravatar_icon(user.email, 40), class: "avatar s32"
13 7 = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do
... ... @@ -16,5 +10,11 @@
16 10 %div.cgray= user.email
17 11  
18 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 33 git_host:
34 34 admin_uri: git@localhost:gitolite-admin
35 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 38 git_user: git
39 39 upload_pack: true
40 40 receive_pack: true
  41 + # host: localhost
41 42 # port: 22
42 43  
43 44 # Git settings
... ...
config/initializers/1_settings.rb
... ... @@ -102,6 +102,10 @@ class Settings &lt; Settingslogic
102 102 git_host['admin_uri'] || 'git@localhost:gitolite-admin'
103 103 end
104 104  
  105 + def gitolite_admin_key
  106 + git_host['gitolite_admin_key'] || 'gitlab'
  107 + end
  108 +
105 109 def default_projects_limit
106 110 app['default_projects_limit'] || 10
107 111 end
... ...
doc/api/projects.md
... ... @@ -112,6 +112,66 @@ Parameters:
112 112 Will return created project with status `201 Created` on success, or `404 Not
113 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 176 ## Project repository branches
117 177  
... ...
doc/installation.md
... ... @@ -113,17 +113,20 @@ Generate key:
113 113 Clone GitLab's fork of the Gitolite source code:
114 114  
115 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 118 Setup:
119 119  
  120 + cd /home/git
  121 + sudo -u git -H mkdir bin
120 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 125 sudo cp /home/gitlab/.ssh/id_rsa.pub /home/git/gitlab.pub
123 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 131 Permissions:
129 132  
... ... @@ -189,8 +192,8 @@ and ensure you have followed all of the above steps carefully.
189 192  
190 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 198 #### Check application status
196 199  
... ...
features/dashboard/dashboard.feature
... ... @@ -15,4 +15,14 @@ Feature: Dashboard
15 15 And I click "Create Merge Request" link
16 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 64 And I fill in issue search with ""
65 65 Then I should see "Release 0.4" in issues
66 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 109 :author => @user,
110 110 :project => project2
111 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 55 end
56 56 fill_in 'issue_search', with: arg1
57 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 22 Given /^I select "(.*?)" as "(.*?)"$/ do |arg1, arg2|
23 23 user = User.find_by_name(arg1)
24 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 27 end
28 28 click_button "Save"
29 29 end
... ...
lib/api/entities.rb
... ... @@ -16,6 +16,11 @@ module Gitlab
16 16 expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at
17 17 end
18 18  
  19 + class UsersProject < Grape::Entity
  20 + expose :user, using: Entities::UserBasic
  21 + expose :project_access
  22 + end
  23 +
19 24 class RepoObject < Grape::Entity
20 25 expose :name, :commit
21 26 end
... ...
lib/api/helpers.rb
... ... @@ -21,5 +21,21 @@ module Gitlab
21 21 def authenticate!
22 22 error!({'message' => '401 Unauthorized'}, 401) unless current_user
23 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 40 end
25 41 end
... ...
lib/api/issues.rb
... ... @@ -79,6 +79,8 @@ module Gitlab
79 79 # PUT /projects/:id/issues/:issue_id
80 80 put ":id/issues/:issue_id" do
81 81 @issue = user_project.issues.find(params[:issue_id])
  82 + authorize! :modify_issue, @issue
  83 +
82 84 parameters = {
83 85 title: (params[:title] || @issue.title),
84 86 description: (params[:description] || @issue.description),
... ...
lib/api/milestones.rb
... ... @@ -61,6 +61,8 @@ module Gitlab
61 61 # Example Request:
62 62 # PUT /projects/:id/milestones/:milestone_id
63 63 put ":id/milestones/:milestone_id" do
  64 + authorize! :admin_milestone, user_project
  65 +
64 66 @milestone = user_project.milestones.find(params[:milestone_id])
65 67 parameters = {
66 68 title: (params[:title] || @milestone.title),
... ...
lib/api/projects.rb
... ... @@ -54,6 +54,58 @@ module Gitlab
54 54 end
55 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 109 # Get a project repository branches
58 110 #
59 111 # Parameters:
... ... @@ -137,6 +189,8 @@ module Gitlab
137 189 # PUT /projects/:id/snippets/:snippet_id
138 190 put ":id/snippets/:snippet_id" do
139 191 @snippet = user_project.snippets.find(params[:snippet_id])
  192 + authorize! :modify_snippet, @snippet
  193 +
140 194 parameters = {
141 195 title: (params[:title] || @snippet.title),
142 196 file_name: (params[:file_name] || @snippet.file_name),
... ... @@ -160,6 +214,8 @@ module Gitlab
160 214 # DELETE /projects/:id/snippets/:snippet_id
161 215 delete ":id/snippets/:snippet_id" do
162 216 @snippet = user_project.snippets.find(params[:snippet_id])
  217 + authorize! :modify_snippet, @snippet
  218 +
163 219 @snippet.destroy
164 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 3 module Gitlab
7 4 class Gitolite
8 5 class AccessDenied < StandardError; end
9   - class InvalidKey < StandardError; end
  6 +
  7 + def config
  8 + Gitlab::GitoliteConfig.new
  9 + end
10 10  
11 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 15 end
16 16 end
17 17  
18 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 22 end
23 23 end
24 24  
25 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 27 end
30 28  
31   - alias_method :create_repository, :update_repository
32   -
33 29 def remove_repository project
34   - configure do |c|
35   - c.destroy_project(project)
36   - end
  30 + config.destroy_project!(project)
37 31 end
38 32  
39 33 def url_to_repo path
40 34 Gitlab.config.ssh_path + "#{path}.git"
41 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 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 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 42 end
202 43 end
... ...
lib/gitlab/backend/gitolite_config.rb 0 → 100644
... ... @@ -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 47 # Note: reference links will only be generated if @project is set
48 48 def gfm(text, html_options = {})
49 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 54 @html_options = html_options
53 55  
... ... @@ -78,9 +80,12 @@ module Gitlab
78 80 #
79 81 # text - Text to parse
80 82 #
  83 + # Note: reference links will only be generated if @project is set
  84 + #
81 85 # Returns parsed text
82 86 def parse(text)
83   - text = text.gsub(REFERENCE_PATTERN) do |match|
  87 + # parse reference links
  88 + text.gsub!(REFERENCE_PATTERN) do |match|
84 89 prefix = $1 || ''
85 90 reference = $2
86 91 identifier = $3 || $4 || $5
... ... @@ -91,9 +96,10 @@ module Gitlab
91 96 else
92 97 match
93 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 103 if valid_emoji?($2)
98 104 image_tag("emoji/#{$2}.png", size: "20x20", class: 'emoji', title: $1, alt: $1)
99 105 else
... ...
lib/gitlab/merge.rb
... ... @@ -21,8 +21,7 @@ module Gitlab
21 21 if output =~ /CONFLICT/
22 22 false
23 23 else
24   - repo.git.push({}, "origin", merge_request.target_branch)
25   - true
  24 + !!repo.git.push({}, "origin", merge_request.target_branch)
26 25 end
27 26 end
28 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 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 8 puts "Found #{repos_to_import.length} repos to import"
11 9  
12 10 imported_count = 0
... ... @@ -14,11 +12,9 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
14 12 failed_count = 0
15 13 repos_to_import.each do |repo_path|
16 14 repo_name = File.basename repo_path
17   - repo_full_path = File.join(Rails.root, repo_path)
18 15  
19 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 19 if Dir.exists? clone_path
24 20 if Project.find_by_code(repo_name)
... ... @@ -30,7 +26,7 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
30 26 end
31 27 else
32 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 30 failed_count += 1
35 31 next
36 32 end
... ... @@ -48,14 +44,17 @@ task :import_projects, [:email] =&gt; :environment do |t, args|
48 44 puts "Finished importing #{imported_count} projects (skipped #{skipped_count}, failed #{failed_count})."
49 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 48 def clone_bare_repo_as_git(existing_path, new_path)
  49 + git_user = Gitlab.config.ssh_user
53 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 52 true
56   - rescue
  53 + rescue Exception=> msg
57 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 58 end
60 59 end
61 60  
... ...
spec/helpers/gitlab_markdown_helper_spec.rb
... ... @@ -247,6 +247,11 @@ describe GitlabMarkdownHelper do
247 247 it "ignores invalid emoji" do
248 248 gfm(":invalid-emoji:").should_not match(/<img/)
249 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 255 end
251 256 end
252 257  
... ...
spec/helpers/tree_helper_spec.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 49 it { @event.branch_name.should == "master" }
50 50 it { @event.author.should == @user }
51 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 74 end
... ...
spec/observers/users_project_observer_spec.rb
... ... @@ -23,6 +23,17 @@ describe UsersProjectObserver do
23 23 Notify.should_receive(:project_access_granted_email).with(users_project.id).and_return(double(deliver: true))
24 24 subject.after_commit(users_project)
25 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 37 it "should called when UsersProject updated" do
27 38 subject.should_receive(:after_commit).once
28 39 UsersProject.observers.enable :users_project_observer do
... ... @@ -40,4 +51,23 @@ describe UsersProjectObserver do
40 51 end
41 52 end
42 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 73 end
... ...
spec/requests/api/projects_spec.rb
... ... @@ -4,8 +4,12 @@ describe Gitlab::API do
4 4 include ApiHelpers
5 5  
6 6 let(:user) { Factory :user }
  7 + let(:user2) { Factory.create(:user) }
  8 + let(:user3) { Factory.create(:user) }
7 9 let!(:project) { Factory :project, owner: user }
8 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 13 before { project.add_access(user, :read) }
10 14  
11 15 describe "GET /projects" do
... ... @@ -104,6 +108,45 @@ describe Gitlab::API do
104 108 end
105 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 150 describe "GET /projects/:id/repository/tags" do
108 151 it "should return an array of project tags" do
109 152 get api("/projects/#{project.code}/repository/tags", user)
... ...
spec/requests/projects_spec.rb
... ... @@ -3,6 +3,16 @@ require &#39;spec_helper&#39;
3 3 describe "Projects" do
4 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 16 describe "GET /projects/show" do
7 17 before do
8 18 @project = Factory :project, owner: @user
... ...
spec/support/gitolite_stub.rb
... ... @@ -17,7 +17,7 @@ module GitoliteStub
17 17 )
18 18  
19 19 gitolite_admin = double(
20   - 'Gitolite::GitoliteAdmin',
  20 + 'Gitolite::GitoliteAdmin',
21 21 config: gitolite_config,
22 22 save: true,
23 23 )
... ... @@ -27,9 +27,21 @@ module GitoliteStub
27 27 end
28 28  
29 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 46 end
35 47 end
... ...