Commit 77bfc591bf5836892be26059d92411f9fbf04be9
Exists in
master
and in
4 other branches
Merge 'master' branch
Showing
65 changed files
with
965 additions
and
435 deletions
Show diff stats
Gemfile
@@ -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" |
Gemfile.lock
@@ -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 | -} |
@@ -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
app/controllers/team_members_controller.rb
@@ -17,13 +17,12 @@ class TeamMembersController < ApplicationController | @@ -17,13 +17,12 @@ class TeamMembersController < 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 < ApplicationDecorator | @@ -8,7 +8,9 @@ class EventDecorator < 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/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 < ActiveRecord::Base | @@ -10,6 +10,8 @@ class Event < 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 < ActiveRecord::Base | @@ -37,7 +39,7 @@ class Event < 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 < ActiveRecord::Base | @@ -84,6 +86,18 @@ class Event < 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 < ActiveRecord::Base | @@ -101,6 +115,10 @@ class Event < 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 < ActiveRecord::Base | @@ -162,7 +162,7 @@ class MergeRequest < 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 < ActiveRecord::Base | @@ -20,6 +20,23 @@ class UsersProject < 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 < ActiveRecord::Observer | @@ -3,4 +3,20 @@ class UsersProjectObserver < 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
app/views/errors/invalid_ssh_key.html.haml
app/views/events/_event.html.haml
@@ -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 < Settingslogic | @@ -102,6 +102,10 @@ class Settings < 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 "(.*?)"$/ do |arg1| | @@ -55,3 +55,27 @@ Given /^I fill in issue search with "(.*?)"$/ 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 |
@@ -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] => :environment do |t, args| | @@ -14,11 +12,9 @@ task :import_projects, [:email] => :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] => :environment do |t, args| | @@ -30,7 +26,7 @@ task :import_projects, [:email] => :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] => :environment do |t, args| | @@ -48,14 +44,17 @@ task :import_projects, [:email] => :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 |
@@ -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 |
@@ -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 |
@@ -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 'spec_helper' | @@ -3,6 +3,16 @@ require 'spec_helper' | ||
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 |