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
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 | -} |
... | ... | @@ -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
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
app/controllers/refs_controller.rb
app/controllers/team_members_controller.rb
... | ... | @@ -17,13 +17,12 @@ class TeamMembersController < 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 < 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/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 < 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 < 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 < 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 < 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 < 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 < 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 < 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
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
app/views/errors/invalid_ssh_key.html.haml
app/views/events/_event.html.haml
... | ... | @@ -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 < 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 "(.*?)"$/ 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 | ... | ... |
... | ... | @@ -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
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] => :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] => :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] => :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 | ... | ... |
... | ... | @@ -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 @@ |
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 @@ |
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 'spec_helper' |
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 | ... | ... |