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