From bc3137ac4961f5f763fe8db2b5bb43bccfa34258 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 29 Sep 2014 21:35:41 +0300 Subject: [PATCH 001/134] Fix milestone link in issue. Closes #174 (gitlab.com). --- app/views/projects/issues/_issue_context.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 8c3f082338..f8f1add2fd 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -19,6 +19,7 @@ = hidden_field_tag :issue_context = f.submit class: 'btn' - elsif issue.milestone - = link_to issue.milestone.title, project_milestone_path + = link_to project_milestone_path(@project, @issue.milestone) do + = @issue.milestone.title - else None From adf04082299a37bc953d93a4d38f9b8c24cc307d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 1 Oct 2014 20:07:28 +0300 Subject: [PATCH 002/134] Fix identation. --- app/views/projects/issues/_issue_context.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index f8f1add2fd..648f459dc9 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -20,6 +20,6 @@ = f.submit class: 'btn' - elsif issue.milestone = link_to project_milestone_path(@project, @issue.milestone) do - = @issue.milestone.title + = @issue.milestone.title - else None From 9bebacd69260b7106bcee42ad7317c7f9c5c5525 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sat, 4 Oct 2014 17:09:06 +0200 Subject: [PATCH 003/134] Dry admin logs. --- app/views/admin/logs/show.html.haml | 87 ++++++++--------------------- lib/gitlab/app_logger.rb | 4 +- lib/gitlab/git_logger.rb | 4 +- lib/gitlab/logger.rb | 4 ++ lib/gitlab/production_logger.rb | 7 +++ lib/gitlab/sidekiq_logger.rb | 7 +++ 6 files changed, 44 insertions(+), 69 deletions(-) create mode 100644 lib/gitlab/production_logger.rb create mode 100644 lib/gitlab/sidekiq_logger.rb diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index b3f8f012f0..384c6ee9af 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,68 +1,25 @@ +- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, + Gitlab::ProductionLogger, Gitlab::SidekiqLogger] %ul.nav.nav-tabs.log-tabs - %li.active - = link_to "githost.log", "#githost", 'data-toggle' => 'tab' - %li - = link_to "application.log", "#application", 'data-toggle' => 'tab' - %li - = link_to "production.log", "#production", 'data-toggle' => 'tab' - %li - = link_to "sidekiq.log", "#sidekiq", 'data-toggle' => 'tab' - + - loggers.each do |klass| + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + = link_to klass::file_name, "##{klass::file_name_noext}", + 'data-toggle' => 'tab' %p.light To prevent performance issues admin logs output the last 2000 lines .tab-content - .tab-pane.active#githost - .file-holder#README - .file-title - %i.fa.fa-file - githost.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::GitLogger.read_latest.each do |line| - %li - %p= line - .tab-pane#application - .file-holder#README - .file-title - %i.fa.fa-file - application.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::AppLogger.read_latest.each do |line| - %li - %p= line - .tab-pane#production - .file-holder#README - .file-title - %i.fa.fa-file - production.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::Logger.read_latest_for('production.log').each do |line| - %li - %p= line - .tab-pane#sidekiq - .file-holder#README - .file-title - %i.fa.fa-file - sidekiq.log - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - Gitlab::Logger.read_latest_for('sidekiq.log').each do |line| - %li - %p= line + - loggers.each do |klass| + .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), + id: klass::file_name_noext } + .file-holder#README + .file-title + %i.fa.fa-file + = klass::file_name + .pull-right + = link_to '#', class: 'log-bottom' do + %i.fa.fa-arrow-down + Scroll down + .file-content.logs + %ol + - klass.read_latest.each do |line| + %li + %p= line diff --git a/lib/gitlab/app_logger.rb b/lib/gitlab/app_logger.rb index 8e4717b46e..dddcb2538f 100644 --- a/lib/gitlab/app_logger.rb +++ b/lib/gitlab/app_logger.rb @@ -1,7 +1,7 @@ module Gitlab class AppLogger < Gitlab::Logger - def self.file_name - 'application.log' + def self.file_name_noext + 'application' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/git_logger.rb b/lib/gitlab/git_logger.rb index fbfed205a0..9e02ccc0f4 100644 --- a/lib/gitlab/git_logger.rb +++ b/lib/gitlab/git_logger.rb @@ -1,7 +1,7 @@ module Gitlab class GitLogger < Gitlab::Logger - def self.file_name - 'githost.log' + def self.file_name_noext + 'githost' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb index 8a73ec5038..59b21149a9 100644 --- a/lib/gitlab/logger.rb +++ b/lib/gitlab/logger.rb @@ -1,5 +1,9 @@ module Gitlab class Logger < ::Logger + def self.file_name + file_name_noext + '.log' + end + def self.error(message) build.error(message) end diff --git a/lib/gitlab/production_logger.rb b/lib/gitlab/production_logger.rb new file mode 100644 index 0000000000..89ce7144b1 --- /dev/null +++ b/lib/gitlab/production_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class ProductionLogger < Gitlab::Logger + def self.file_name_noext + 'production' + end + end +end diff --git a/lib/gitlab/sidekiq_logger.rb b/lib/gitlab/sidekiq_logger.rb new file mode 100644 index 0000000000..c1dab87a43 --- /dev/null +++ b/lib/gitlab/sidekiq_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class SidekiqLogger < Gitlab::Logger + def self.file_name_noext + 'sidekiq' + end + end +end From ca840e8769fa75f786bf01e1ac839a81bd8ac32b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 6 Oct 2014 12:28:10 +0000 Subject: [PATCH 004/134] fonts: Added "DejaVu Sans Mono" and "Ubuntu Mono" Everything is better than "Courier New" ... --- app/assets/stylesheets/main/fonts.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/main/fonts.scss b/app/assets/stylesheets/main/fonts.scss index d90274a0db..f945aaca84 100644 --- a/app/assets/stylesheets/main/fonts.scss +++ b/app/assets/stylesheets/main/fonts.scss @@ -1,3 +1,3 @@ /** Typo **/ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; From 10783f4d7b1b4b8f1ade255176c0d1b3667c66ae Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Tue, 7 Oct 2014 23:41:02 +0200 Subject: [PATCH 005/134] Remove unneeded app/finders config.autoload path Every directory under app/ is searched by default --- config/application.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index e36df913d0..e29c24249a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,6 @@ module Gitlab # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{config.root}/lib - #{config.root}/app/finders #{config.root}/app/models/hooks #{config.root}/app/models/concerns #{config.root}/app/models/project_services From 166c215a75b50ce62fd40297f52e366df7dc9103 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Fri, 3 Oct 2014 00:13:23 +0200 Subject: [PATCH 006/134] Make new and edit file submit more uniform --- app/views/projects/_commit_button.html.haml | 9 +++++++++ app/views/projects/edit_tree/show.html.haml | 15 +++++---------- app/views/projects/new_tree/show.html.haml | 11 +++-------- features/project/source/browse_files.feature | 4 ++-- features/steps/project/source/browse_files.rb | 4 ++-- 5 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 app/views/projects/_commit_button.html.haml diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml new file mode 100644 index 0000000000..fd8320adb8 --- /dev/null +++ b/app/views/projects/_commit_button.html.haml @@ -0,0 +1,9 @@ +.form-actions + .commit-button-annotation + = button_tag 'Commit Changes', + class: 'btn commit-btn js-commit-button btn-create' + .message + to branch + %strong= ref + = link_to 'Cancel', cancel_path, + class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/edit_tree/show.html.haml index a863f7420a..5ccde05063 100644 --- a/app/views/projects/edit_tree/show.html.haml +++ b/app/views/projects/edit_tree/show.html.haml @@ -23,16 +23,11 @@ %i.fa.fa-spinner.fa-spin = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" - .form-actions - = hidden_field_tag 'last_commit', @last_commit - = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - .commit-button-annotation - = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-primary' - .message - to branch - %strong= @ref - = link_to "Cancel", @after_edit_path, class: "btn btn-cancel", data: { confirm: leave_edit_message} + = hidden_field_tag 'last_commit', @last_commit + = hidden_field_tag 'content', '', id: "file-content" + = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = render 'projects/commit_button', ref: @ref, + cancel_path: @after_edit_path :javascript ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace") diff --git a/app/views/projects/new_tree/show.html.haml b/app/views/projects/new_tree/show.html.haml index 49c504c104..c47c0a3f64 100644 --- a/app/views/projects/new_tree/show.html.haml +++ b/app/views/projects/new_tree/show.html.haml @@ -27,14 +27,9 @@ .file-content.code %pre#editor= params[:content] - .form-actions - = hidden_field_tag 'content', '', id: "file-content" - .commit-button-annotation - = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-create' - .message - to branch - %strong= @ref - = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-cancel", data: { confirm: leave_edit_message} + = hidden_field_tag 'content', '', id: 'file-content' + = render 'projects/commit_button', ref: @ref, + cancel_path: project_tree_path(@project, @id) :javascript ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict") diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 20ef7ac570..8ff2f583b3 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -30,7 +30,7 @@ Feature: Project Source Browse files And I edit code And I fill the new file name And I fill the commit message - And I click on "Commit changes" + And I click on "Commit Changes" Then I am redirected to the new file And I should see its new content @@ -46,7 +46,7 @@ Feature: Project Source Browse files And I click button "Edit" And I edit code And I fill the commit message - And I click on "Commit changes" + And I click on "Commit Changes" Then I am redirected to the ".gitignore" And I should see its new content diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 0642302e79..20f8f6c24a 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -69,8 +69,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_link 'Diff' end - step 'I click on "Commit changes"' do - click_button 'Commit changes' + step 'I click on "Commit Changes"' do + click_button 'Commit Changes' end step 'I click on "Remove"' do From deffd2e5385d4aca161e3047dc551ae8bbdfb0f8 Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Thu, 9 Oct 2014 09:29:10 +0200 Subject: [PATCH 007/134] set the development ruby version automatically --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..ac2cdeba01 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.1.3 From f7dc15c6bdb75a5f611c389ece3fe251f94a2d8f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 9 Oct 2014 11:49:28 +0200 Subject: [PATCH 008/134] Revert "Up version of html-pipeline-gitlab." This reverts commit 17835f095fb10168d5d2f6b2fd4c136844c2bfd1. --- Gemfile.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3eb52d984d..babb23ed60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,10 +241,9 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.3) - gitlab_emoji (~> 0.0.1) + html-pipeline-gitlab (0.1.0) + gitlab_emoji (~> 0.0.1.1) html-pipeline (~> 1.11.0) - sanitize (~> 2.1) http_parser.rb (0.5.3) httparty (0.13.0) json (~> 1.8) From b4828f4cf6405d27c01cc5be42334dd29a27285b Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 9 Oct 2014 13:33:38 +0200 Subject: [PATCH 009/134] Enable markdown pipeline filters from inside gitlab. --- lib/gitlab/markdown.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 17512a5165..d3e9bafb06 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -70,14 +70,17 @@ module Gitlab insert_piece($1) end - # Context passed to the markdoqwn pipeline - markdown_context = { - asset_root: File.join(root_url, - Gitlab::Application.config.assets.prefix) - } + # Used markdown pipelines in GitLab: + # GitlabEmojiFilter - performs emoji replacement. + # + # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters + filters = [ + HTML::Pipeline::Gitlab::GitlabEmojiFilter + ] - result = HTML::Pipeline::Gitlab::MarkdownPipeline.call(text, - markdown_context) + markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline + + result = markdown_pipeline.call(text) text = result[:output].to_html(save_with: 0) allowed_attributes = ActionView::Base.sanitized_allowed_attributes From 099cf3558f9e41022ac38d2f8226bdbe3c9aa470 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 9 Oct 2014 14:02:10 +0200 Subject: [PATCH 010/134] Failing test for apostrophe at the end of user mention on project with issue iid 39. --- spec/helpers/gitlab_markdown_helper_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 15033f0743..f7b87f2966 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -530,6 +530,16 @@ describe GitlabMarkdownHelper do markdown(actual).should match(%r{
  • light by @#{member.user.username}
  • }) end + it "should not link the apostrophe to issue 39" do + project.team << [user, :master] + project.issues.stub(:where).with(iid: '39').and_return([issue]) + + actual = "Yes, it is @#{member.user.username}'s task." + expected = /Yes, it is @#{member.user.username}<\/a>'s task/ + markdown(actual).should match(expected) + end + + it "should handle references in " do actual = "Apply _!#{merge_request.iid}_ ASAP" From 64e72af3cb65731c84e1aa27b68a04fe378bebd9 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 9 Oct 2014 14:20:49 +0200 Subject: [PATCH 011/134] Replace apostrophe with right single quote to avoid markdown interpretation as issue 39. --- lib/redcarpet/render/gitlab_html.rb | 5 +++++ spec/helpers/gitlab_markdown_helper_spec.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index c3378d6a18..53c5a1e09c 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -10,6 +10,11 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML super options end + def normal_text(text) + return text unless text.present? + text.gsub("'", "’") + end + def block_code(code, language) # New lines are placed to fix an rendering issue # with code wrapped inside

    tag for next case: diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index f7b87f2966..55270a9c20 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -535,7 +535,7 @@ describe GitlabMarkdownHelper do project.issues.stub(:where).with(iid: '39').and_return([issue]) actual = "Yes, it is @#{member.user.username}'s task." - expected = /Yes, it is @#{member.user.username}<\/a>'s task/ + expected = /Yes, it is @#{member.user.username}<\/a>’s task/ markdown(actual).should match(expected) end @@ -566,7 +566,7 @@ describe GitlabMarkdownHelper do it "should leave inline code untouched" do markdown("\nDon't use `$#{snippet.id}` here.\n").should == - "

    Don't use $#{snippet.id} here.

    \n" + "

    Don’t use $#{snippet.id} here.

    \n" end it "should leave ref-like autolinks untouched" do From d567dd33db06db8ef68d7603afc14670ba35240f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 9 Oct 2014 16:33:03 +0300 Subject: [PATCH 012/134] Bump gitlab-grit version with improved timeout feature Signed-off-by: Dmitriy Zaporozhets --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index babb23ed60..1de2911fb1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,7 +168,7 @@ GEM multi_json gitlab-grack (2.0.0.pre) rack (~> 1.5.1) - gitlab-grit (2.6.11) + gitlab-grit (2.6.12) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) From 47f539f5a6a930b2cfd4f9834b4d1bd5e1c180cb Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 8 Oct 2014 16:44:25 +0300 Subject: [PATCH 013/134] Snippets: public/internal/private --- .../projects/snippets_controller.rb | 7 +- app/controllers/snippets_controller.rb | 42 +++++---- app/finders/snippets_finder.rb | 61 ++++++++++++ app/helpers/visibility_level_helper.rb | 17 ++++ app/models/project_team.rb | 4 + app/models/snippet.rb | 16 +++- app/views/shared/snippets/_form.html.haml | 18 +--- .../snippets/_visibility_level.html.haml | 27 ++++++ .../snippets/current_user_index.html.haml | 5 + app/views/snippets/index.html.haml | 10 +- app/views/snippets/user_index.html.haml | 5 +- ...7100818_add_visibility_level_to_snippet.rb | 21 +++++ db/schema.rb | 7 +- features/snippets/discover.feature | 2 + features/snippets/user.feature | 13 ++- features/steps/shared/snippet.rb | 16 +++- features/steps/snippets/discover.rb | 4 + features/steps/snippets/user.rb | 14 +++ spec/finders/snippets_finder_spec.rb | 94 +++++++++++++++++++ spec/models/project_team_spec.rb | 4 + 20 files changed, 332 insertions(+), 55 deletions(-) create mode 100644 app/finders/snippets_finder.rb create mode 100644 app/views/shared/snippets/_visibility_level.html.haml create mode 100644 db/migrate/20141007100818_add_visibility_level_to_snippet.rb create mode 100644 spec/finders/snippets_finder_spec.rb diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index cba058fe21..9d5dd8a95c 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -17,7 +17,10 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = @project.snippets.fresh.non_expired + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_project, + project: @project + }) end def new @@ -88,6 +91,6 @@ class Projects::SnippetsController < Projects::ApplicationController end def snippet_params - params.require(:project_snippet).permit(:title, :content, :file_name, :private) + params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level) end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 5904dbbced..30fb4c5552 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -9,12 +9,14 @@ class SnippetsController < ApplicationController before_filter :set_title + skip_before_filter :authenticate_user!, only: [:index, :user_index] + respond_to :html - layout 'navless' + layout :determine_layout def index - @snippets = Snippet.are_internal.fresh.non_expired.page(params[:page]).per(20) + @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20) end def user_index @@ -22,22 +24,11 @@ class SnippetsController < ApplicationController render_404 and return unless @user - @snippets = @user.snippets.fresh.non_expired - - if @user == current_user - @snippets = case params[:scope] - when 'are_internal' then - @snippets.are_internal - when 'are_private' then - @snippets.are_private - else - @snippets - end - else - @snippets = @snippets.are_internal - end - - @snippets = @snippets.page(params[:page]).per(20) + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope]}). + page(params[:page]).per(20) if @user == current_user render 'current_user_index' @@ -95,7 +86,14 @@ class SnippetsController < ApplicationController protected def snippet - @snippet ||= PersonalSnippet.where('author_id = :user_id or private is false', user_id: current_user.id).find(params[:id]) + @snippet ||= if current_user + PersonalSnippet.where("author_id = ? OR visibility_level IN (?)", + current_user.id, + [Snippet::PUBLIC, Snippet::INTERNAL]). + find(params[:id]) + else + PersonalSnippet.are_public.find(params[:id]) + end end def authorize_modify_snippet! @@ -111,6 +109,10 @@ class SnippetsController < ApplicationController end def snippet_params - params.require(:personal_snippet).permit(:title, :content, :file_name, :private) + params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level) + end + + def determine_layout + current_user ? 'navless' : 'public_users' end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb new file mode 100644 index 0000000000..fda375aca2 --- /dev/null +++ b/app/finders/snippets_finder.rb @@ -0,0 +1,61 @@ +class SnippetsFinder + def execute(current_user, params = {}) + filter = params[:filter] + + case filter + when :all then + snippets(current_user).fresh.non_expired + when :by_user then + by_user(current_user, params[:user], params[:scope]) + when :by_project + by_project(current_user, params[:project]) + end + end + + private + + def snippets(current_user) + if current_user + Snippet.public_and_internal + else + # Not authenticated + # + # Return only: + # public snippets + Snippet.are_public + end + end + + def by_user(current_user, user, scope) + snippets = user.snippets.fresh.non_expired + + if user == current_user + snippets = case scope + when 'are_internal' then + snippets.are_internal + when 'are_private' then + snippets.are_private + when 'are_public' then + snippets.are_public + else + snippets + end + else + snippets = snippets.public_and_internal + end + end + + def by_project(current_user, project) + snippets = project.snippets.fresh.non_expired + + if current_user + if project.team.member?(current_user.id) + snippets + else + snippets.public_and_internal + end + else + snippets.are_public + end + end +end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 8b83b8ff64..deb9c8b4d4 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -28,6 +28,23 @@ module VisibilityLevelHelper end end + def snippet_visibility_level_description(level) + capture_haml do + haml_tag :span do + case level + when Gitlab::VisibilityLevel::PRIVATE + haml_concat "The snippet is visible only for me" + when Gitlab::VisibilityLevel::INTERNAL + haml_concat "The snippet is visible for any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + haml_concat "The snippet can be accessed" + haml_concat "without any" + haml_concat "authentication." + end + end + end + end + def visibility_level_icon(level) case level when Gitlab::VisibilityLevel::PRIVATE diff --git a/app/models/project_team.rb b/app/models/project_team.rb index e065554d3b..657ee23ae2 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -133,6 +133,10 @@ class ProjectTeam max_tm_access(user.id) == Gitlab::Access::MASTER end + def member?(user_id) + !!find_tm(user_id) + end + def max_tm_access(user_id) access = [] access << project.project_members.find_by(user_id: user_id).try(:access_field) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index addde2d106..974c239da5 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -17,8 +17,9 @@ class Snippet < ActiveRecord::Base include Linguist::BlobHelper + include Gitlab::VisibilityLevel - default_value_for :private, true + default_value_for :visibility_level, Snippet::PRIVATE belongs_to :author, class_name: "User" @@ -30,10 +31,13 @@ class Snippet < ActiveRecord::Base validates :title, presence: true, length: { within: 0..255 } validates :file_name, presence: true, length: { within: 0..255 } validates :content, presence: true + validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } # Scopes - scope :are_internal, -> { where(private: false) } - scope :are_private, -> { where(private: true) } + scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } + scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } + scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } + scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } @@ -66,6 +70,10 @@ class Snippet < ActiveRecord::Base expires_at && expires_at < Time.current end + def visibility_level_field + visibility_level + end + class << self def search(query) where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") @@ -76,7 +84,7 @@ class Snippet < ActiveRecord::Base end def accessible_to(user) - where('private = ? OR author_id = ?', false, user) + where('visibility_level IN (?) OR author_id = ?', [Snippet::INTERNAL, Snippet::PUBLIC], user) end end end diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index f4d74045f7..f729f129e4 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -10,22 +10,8 @@ = f.label :title, class: 'control-label' .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true - - unless @snippet.respond_to?(:project) - .form-group - = f.label "Access", class: 'control-label' - .col-sm-10 - = f.label :private_true, class: 'radio-label' do - = f.radio_button :private, true - %span - %strong Private - (only you can see this snippet) - %br - = f.label :private_false, class: 'radio-label' do - = f.radio_button :private, false - %span - %strong Internal - (GitLab users can see this snippet) - + = render "shared/snippets/visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true + .form-group .file-editor = f.label :file_name, "File", class: 'control-label' diff --git a/app/views/shared/snippets/_visibility_level.html.haml b/app/views/shared/snippets/_visibility_level.html.haml new file mode 100644 index 0000000000..9acff18e45 --- /dev/null +++ b/app/views/shared/snippets/_visibility_level.html.haml @@ -0,0 +1,27 @@ +.form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + .col-sm-10 + - if can_change_visibility_level + - Gitlab::VisibilityLevel.values.each do |level| + .radio + - restricted = restricted_visibility_levels.include?(level) + = f.radio_button :visibility_level, level, disabled: restricted + = label "#{dom_class(@snippet)}_visibility_level", level do + = visibility_level_icon(level) + .option-title + = visibility_level_label(level) + .option-descr + = snippet_visibility_level_description(level) + - unless restricted_visibility_levels.empty? + .col-sm-10 + %span.info + Some visibility level settings have been restricted by the administrator. + - else + .col-sm-10 + %span.info + = visibility_level_icon(visibility_level) + %strong + = visibility_level_label(visibility_level) + .light= visibility_level_description(visibility_level) diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index 14b5b072ec..b2b7ea4df0 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -28,6 +28,11 @@ Internal %span.pull-right = @user.snippets.are_internal.count + = nav_tab :scope, 'are_public' do + = link_to user_snippets_path(@user, scope: 'are_public') do + Public + %span.pull-right + = @user.snippets.are_public.count .col-md-9.my-snippets = render 'snippets' diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index cea2517a8e..0d71c41e2e 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -2,10 +2,12 @@ Public snippets .pull-right - = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do - Add new snippet - = link_to user_snippets_path(current_user), class: "btn btn-grouped" do - My snippets + + - if current_user + = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do + Add new snippet + = link_to user_snippets_path(current_user), class: "btn btn-grouped" do + My snippets %p.light Public snippets created by you and other users are listed here diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index 1cb53ec6a2..67f3a68aa2 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -4,8 +4,9 @@ %span \/ Snippets - = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do - Add new snippet + - if current_user + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet %hr diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb new file mode 100644 index 0000000000..7f125acb5d --- /dev/null +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -0,0 +1,21 @@ +class AddVisibilityLevelToSnippet < ActiveRecord::Migration + def up + add_column :snippets, :visibility_level, :integer, :default => 0, :null => false + + Snippet.where(private: true).update_all(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + Snippet.where(private: false).update_all(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + + add_index :snippets, :visibility_level + + remove_column :snippets, :private + end + + def down + add_column :snippets, :private, :boolean, :default => false, :null => false + + Snippet.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).update_all(private: false) + Snippet.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).update_all(private: true) + + remove_column :snippets, :visibility_level + end +end diff --git a/db/schema.rb b/db/schema.rb index 84fd125667..8ddebc5132 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141006143943) do +ActiveRecord::Schema.define(version: 20141007100818) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -293,20 +293,21 @@ ActiveRecord::Schema.define(version: 20141006143943) do create_table "snippets", force: true do |t| t.string "title" t.text "content" - t.integer "author_id", null: false + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" - t.boolean "private", default: true, null: false t.string "type" + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree + add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree create_table "taggings", force: true do |t| t.integer "tag_id" diff --git a/features/snippets/discover.feature b/features/snippets/discover.feature index 5094062c8c..1a7e132ea2 100644 --- a/features/snippets/discover.feature +++ b/features/snippets/discover.feature @@ -4,8 +4,10 @@ Feature: Snippets Discover Given I sign in as a user And I have public "Personal snippet one" snippet And I have private "Personal snippet private" snippet + And I have internal "Personal snippet internal" snippet Scenario: I should see snippets Given I visit snippets page Then I should see "Personal snippet one" in snippets + And I should see "Personal snippet internal" in snippets And I should not see "Personal snippet private" in snippets diff --git a/features/snippets/user.feature b/features/snippets/user.feature index ae34e8e7ff..5b5dadb7b3 100644 --- a/features/snippets/user.feature +++ b/features/snippets/user.feature @@ -4,20 +4,31 @@ Feature: Snippets User Given I sign in as a user And I have public "Personal snippet one" snippet And I have private "Personal snippet private" snippet + And I have internal "Personal snippet internal" snippet Scenario: I should see all my snippets Given I visit my snippets page Then I should see "Personal snippet one" in snippets And I should see "Personal snippet private" in snippets + And I should see "Personal snippet internal" in snippets Scenario: I can see only my private snippets Given I visit my snippets page And I click "Private" filter Then I should not see "Personal snippet one" in snippets + And I should not see "Personal snippet internal" in snippets And I should see "Personal snippet private" in snippets Scenario: I can see only my public snippets Given I visit my snippets page - And I click "Internal" filter + And I click "Public" filter Then I should see "Personal snippet one" in snippets And I should not see "Personal snippet private" in snippets + And I should not see "Personal snippet internal" in snippets + + Scenario: I can see only my internal snippets + Given I visit my snippets page + And I click "Internal" filter + Then I should see "Personal snippet internal" in snippets + And I should not see "Personal snippet private" in snippets + And I should not see "Personal snippet one" in snippets diff --git a/features/steps/shared/snippet.rb b/features/steps/shared/snippet.rb index 5a27e8750c..432f32defc 100644 --- a/features/steps/shared/snippet.rb +++ b/features/steps/shared/snippet.rb @@ -6,7 +6,7 @@ module SharedSnippet title: "Personal snippet one", content: "Test content", file_name: "snippet.rb", - private: false, + visibility_level: Snippet::PUBLIC, author: current_user) end @@ -15,9 +15,19 @@ module SharedSnippet title: "Personal snippet private", content: "Provate content", file_name: "private_snippet.rb", - private: true, + visibility_level: Snippet::PRIVATE, author: current_user) end + + step 'I have internal "Personal snippet internal" snippet' do + create(:personal_snippet, + title: "Personal snippet internal", + content: "Provate content", + file_name: "internal_snippet.rb", + visibility_level: Snippet::INTERNAL, + author: current_user) + end + step 'I have a public many lined snippet' do create(:personal_snippet, title: 'Many lined snippet', @@ -38,7 +48,7 @@ module SharedSnippet |line fourteen END file_name: 'many_lined_snippet.rb', - private: true, + visibility_level: Snippet::PUBLIC, author: current_user) end end diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb index 42bccafcc8..2667c1e3d4 100644 --- a/features/steps/snippets/discover.rb +++ b/features/steps/snippets/discover.rb @@ -7,6 +7,10 @@ class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps page.should have_content "Personal snippet one" end + step 'I should see "Personal snippet internal" in snippets' do + page.should have_content "Personal snippet internal" + end + step 'I should not see "Personal snippet private" in snippets' do page.should_not have_content "Personal snippet private" end diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index c41bc43614..866f637ab6 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -15,6 +15,10 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps page.should have_content "Personal snippet private" end + step 'I should see "Personal snippet internal" in snippets' do + page.should have_content "Personal snippet internal" + end + step 'I should not see "Personal snippet one" in snippets' do page.should_not have_content "Personal snippet one" end @@ -23,6 +27,10 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps page.should_not have_content "Personal snippet private" end + step 'I should not see "Personal snippet internal" in snippets' do + page.should_not have_content "Personal snippet internal" + end + step 'I click "Internal" filter' do within('.nav-stacked') do click_link "Internal" @@ -35,6 +43,12 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps end end + step 'I click "Public" filter' do + within('.nav-stacked') do + click_link "Public" + end + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb new file mode 100644 index 0000000000..5af7696818 --- /dev/null +++ b/spec/finders/snippets_finder_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe SnippetsFinder do + let(:user) { create :user } + let(:user1) { create :user } + let(:group) { create :group } + + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } + + + context ':all filter' do + before do + @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) + @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) + @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + end + + it "returns all private and internal snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :all) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns all public snippets" do + snippets = SnippetsFinder.new.execute(nil, filter: :all) + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + end + + context ':by_user filter' do + before do + @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) + @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) + @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + end + + it "returns all public and internal snippets" do + snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns internal snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") + snippets.should include(@snippet2) + snippets.should_not include(@snippet1, @snippet3) + end + + it "returns private snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") + snippets.should include(@snippet1) + snippets.should_not include(@snippet2, @snippet3) + end + + it "returns public snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + + it "returns all snippets" do + snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) + snippets.should include(@snippet1, @snippet2, @snippet3) + end + end + + context 'by_project filter' do + before do + @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) + @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) + @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + end + + it "returns public snippets for unauthorized user" do + snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1) + snippets.should include(@snippet3) + snippets.should_not include(@snippet1, @snippet2) + end + + it "returns public and internal snippets for none project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets.should include(@snippet2, @snippet3) + snippets.should_not include(@snippet1) + end + + it "returns all snippets for project members" do + project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets.should include(@snippet1, @snippet2, @snippet3) + end + end +end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 34c1a686c9..bbf50b654f 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -27,6 +27,8 @@ describe ProjectTeam do it { project.team.master?(guest).should be_false } it { project.team.master?(reporter).should be_false } it { project.team.master?(nonmember).should be_false } + it { project.team.member?(nonmember).should be_false } + it { project.team.member?(guest).should be_true } end end @@ -60,6 +62,8 @@ describe ProjectTeam do it { project.team.master?(guest).should be_true } it { project.team.master?(reporter).should be_false } it { project.team.master?(nonmember).should be_false } + it { project.team.member?(nonmember).should be_false } + it { project.team.member?(guest).should be_true } end end end From a912308340ec70f13de98ae5116a8d71929a995f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 9 Oct 2014 16:12:17 +0200 Subject: [PATCH 014/134] Add a test for apostrophe in code blocks. --- spec/helpers/gitlab_markdown_helper_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 55270a9c20..0784834924 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -539,6 +539,14 @@ describe GitlabMarkdownHelper do markdown(actual).should match(expected) end + it "should not link the apostrophe to issue 39 in code blocks" do + project.team << [user, :master] + project.issues.stub(:where).with(iid: '39').and_return([issue]) + + actual = "Yes, `it is @#{member.user.username}'s task.`" + expected = /Yes, it is @gfm\'s task.<\/code>/ + markdown(actual).should match(expected) + end it "should handle references in " do actual = "Apply _!#{merge_request.iid}_ ASAP" From 82c938ad75d40a62abfd7b4bd603e57bef6555f5 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 9 Oct 2014 18:22:20 +0300 Subject: [PATCH 015/134] annotate --- app/models/member.rb | 15 +++++++++++++ app/models/members/group_member.rb | 15 +++++++++++++ app/models/members/project_member.rb | 15 +++++++++++++ app/models/personal_snippet.rb | 22 +++++++++---------- .../project_services/assembla_service.rb | 16 +++++++------- .../project_services/buildbox_service.rb | 16 +++++++------- .../project_services/campfire_service.rb | 16 +++++++------- app/models/project_services/ci_service.rb | 16 +++++++------- .../emails_on_push_service.rb | 16 +++++++------- .../project_services/flowdock_service.rb | 16 +++++++------- .../project_services/gemnasium_service.rb | 16 +++++++------- .../project_services/gitlab_ci_service.rb | 16 +++++++------- .../project_services/hipchat_service.rb | 16 +++++++------- .../pivotaltracker_service.rb | 16 +++++++------- app/models/project_services/slack_service.rb | 16 +++++++------- app/models/project_snippet.rb | 22 +++++++++---------- app/models/service.rb | 17 +++++++------- app/models/snippet.rb | 22 +++++++++---------- spec/models/assembla_service_spec.rb | 16 +++++++------- spec/models/buildbox_service_spec.rb | 21 +++++++----------- spec/models/flowdock_service_spec.rb | 16 +++++++------- spec/models/gemnasium_service_spec.rb | 16 +++++++------- spec/models/gitlab_ci_service_spec.rb | 16 +++++++------- spec/models/group_member_spec.rb | 8 ++++--- spec/models/project_member_spec.rb | 10 +++++---- spec/models/project_snippet_spec.rb | 22 +++++++++---------- spec/models/service_spec.rb | 16 +++++++------- spec/models/slack_service_spec.rb | 16 +++++++------- spec/models/snippet_spec.rb | 22 +++++++++---------- 29 files changed, 264 insertions(+), 219 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index 7dc13c18bf..671ef466ba 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class Member < ActiveRecord::Base include Notifiable include Gitlab::Access diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index e72393c427..b7f296b13f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class GroupMember < Member SOURCE_TYPE = 'Namespace' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 71525f9196..30c09f768d 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: members +# +# id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null +# user_id :integer not null +# notification_level :integer not null +# type :string(255) +# created_at :datetime +# updated_at :datetime +# + class ProjectMember < Member SOURCE_TYPE = 'Project' diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index a3c0d201ee..9cee3b70cb 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class PersonalSnippet < Snippet diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 3421a0330a..0b90a14f39 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class AssemblaService < Service diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 7904177f9d..b0f8e28c97 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# property :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class BuildboxService < CiService diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 2d8950db49..0736ddab99 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class CampfireService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 829f495abc..b1d5e49ede 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # # Base class for CI services diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 5c4537cfca..b9071b9829 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class EmailsOnPushService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 4d11b00c19..0020b4482e 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require "flowdock-git-hook" diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 7b6c87e4ce..6d2fc06a5d 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 001b11c596..a897c4ab76 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# property :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class GitlabCiService < CiService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 3a1ba168e6..4078938cdb 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class HipchatService < Service diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 3aa928b92a..09e114f9cc 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class PivotaltrackerService < Service diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index dfa1e9c982..95f3ddcef4 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # class SlackService < Service diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 14c8804642..9e2c1b0e18 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class ProjectSnippet < Snippet diff --git a/app/models/service.rb b/app/models/service.rb index 1f3a652047..c489c1e96e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,14 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# # To add new service you should build a class inherited from Service # and implement a set of methods diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 974c239da5..a47fbca326 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # class Snippet < ActiveRecord::Base diff --git a/spec/models/assembla_service_spec.rb b/spec/models/assembla_service_spec.rb index 0ef475b87c..4300090eb1 100644 --- a/spec/models/assembla_service_spec.rb +++ b/spec/models/assembla_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/buildbox_service_spec.rb b/spec/models/buildbox_service_spec.rb index 477ee71895..1d9ca51be1 100644 --- a/spec/models/buildbox_service_spec.rb +++ b/spec/models/buildbox_service_spec.rb @@ -2,19 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# token :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# project_url :string(255) -# subdomain :string(255) -# room :string(255) -# recipients :text -# api_key :string(255) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/flowdock_service_spec.rb b/spec/models/flowdock_service_spec.rb index 710b8cba50..5540f0fa98 100644 --- a/spec/models/flowdock_service_spec.rb +++ b/spec/models/flowdock_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/gemnasium_service_spec.rb b/spec/models/gemnasium_service_spec.rb index 5de645cdf3..60ffa6f8b0 100644 --- a/spec/models/gemnasium_service_spec.rb +++ b/spec/models/gemnasium_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/gitlab_ci_service_spec.rb b/spec/models/gitlab_ci_service_spec.rb index e4cd8bb90c..ebc377047b 100644 --- a/spec/models/gitlab_ci_service_spec.rb +++ b/spec/models/gitlab_ci_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/group_member_spec.rb b/spec/models/group_member_spec.rb index 6acbc9bb4a..38657de679 100644 --- a/spec/models/group_member_spec.rb +++ b/spec/models/group_member_spec.rb @@ -1,14 +1,16 @@ # == Schema Information # -# Table name: group_members +# Table name: members # # id :integer not null, primary key # access_level :integer not null -# group_id :integer not null +# source_id :integer not null +# source_type :string(255) not null # user_id :integer not null +# notification_level :integer not null +# type :string(255) # created_at :datetime # updated_at :datetime -# notification_level :integer default(3), not null # require 'spec_helper' diff --git a/spec/models/project_member_spec.rb b/spec/models/project_member_spec.rb index 0178d065e5..9b5f89b6d7 100644 --- a/spec/models/project_member_spec.rb +++ b/spec/models/project_member_spec.rb @@ -1,14 +1,16 @@ # == Schema Information # -# Table name: project_members +# Table name: members # # id :integer not null, primary key +# access_level :integer not null +# source_id :integer not null +# source_type :string(255) not null # user_id :integer not null -# project_id :integer not null +# notification_level :integer not null +# type :string(255) # created_at :datetime # updated_at :datetime -# project_access :integer default(0), not null -# notification_level :integer default(3), not null # require 'spec_helper' diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index e4df934460..a6e1d9eef5 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # require 'spec_helper' diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 480aeabf67..c96f2b2052 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/slack_service_spec.rb b/spec/models/slack_service_spec.rb index 3e555193b3..95df38d940 100644 --- a/spec/models/slack_service_spec.rb +++ b/spec/models/slack_service_spec.rb @@ -2,14 +2,14 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text # require 'spec_helper' diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index d179e9516e..1ef2c512c1 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -2,17 +2,17 @@ # # Table name: snippets # -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# expires_at :datetime -# private :boolean default(TRUE), not null -# type :string(255) +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# visibility_level :integer default(0), not null # require 'spec_helper' From 85f1e8b84a7aab0b3ca116ccf1b5f795e9a2af82 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Thu, 9 Oct 2014 22:33:29 -0500 Subject: [PATCH 016/134] Remove unused method --- app/services/issues/update_service.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 5b2746ffec..0ee9635ed9 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -33,12 +33,5 @@ module Issues issue end - - private - - def update_task(issue, params, checked) - issue.update_nth_task(params[:task_num].to_i, checked) - params.except!(:task_num) - end end end From 1d14676e0ce0db006058e02aa0ceedf9c05e5625 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 08:24:58 +0200 Subject: [PATCH 017/134] Substitute right single quote back with apostrophe. --- lib/redcarpet/render/gitlab_html.rb | 1 + spec/helpers/gitlab_markdown_helper_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 53c5a1e09c..511619631f 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -49,6 +49,7 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) + full_document.gsub!("’", "'") unless @template.instance_variable_get("@project_wiki") || @project.nil? full_document = h.create_relative_links(full_document) end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 0784834924..f5e68687b5 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -535,7 +535,7 @@ describe GitlabMarkdownHelper do project.issues.stub(:where).with(iid: '39').and_return([issue]) actual = "Yes, it is @#{member.user.username}'s task." - expected = /Yes, it is @#{member.user.username}<\/a>’s task/ + expected = /Yes, it is @#{member.user.username}<\/a>'s task/ markdown(actual).should match(expected) end @@ -574,7 +574,7 @@ describe GitlabMarkdownHelper do it "should leave inline code untouched" do markdown("\nDon't use `$#{snippet.id}` here.\n").should == - "

    Don’t use $#{snippet.id} here.

    \n" + "

    Don't use $#{snippet.id} here.

    \n" end it "should leave ref-like autolinks untouched" do From fc2adfb6e4af59e45809661e32be0d2ad3158503 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 08:32:05 +0200 Subject: [PATCH 018/134] Add a comment why this is done. --- lib/redcarpet/render/gitlab_html.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 511619631f..54d740908d 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -10,6 +10,12 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML super options end + # If project has issue number 39, apostrophe will be linked in + # regular text to the issue as Redcarpet will convert apostrophe to + # #39; + # We replace apostrophe with right single quote before Redcarpet + # does the processing and put the apostrophe back in postprocessing. + # This only influences regular text, code blocks are untouched. def normal_text(text) return text unless text.present? text.gsub("'", "’") From 8ddb2be69a7dfc17bc3819ab78c6fd159b7ff5b4 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 09:38:39 +0200 Subject: [PATCH 019/134] Revert "update ssl_ciphers" This reverts commit c41e5f5018d059a9c57d2c19088e6c274cc60e10. --- lib/support/nginx/gitlab-ssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 5f1afe6575..e2c03105ba 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -65,7 +65,7 @@ server { ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; - ssl_ciphers 'AES256+EECDH:AES256+EDH'; + ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4'; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_session_cache builtin:1000 shared:SSL:10m; From be14c7a83fa3c591038746a6e94f8de9e5058f04 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 09:44:08 +0200 Subject: [PATCH 020/134] Add a comment on why we are using backward compatible ciphers. --- lib/support/nginx/gitlab-ssl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index e2c03105ba..d3fb467ef2 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -19,7 +19,7 @@ ## - installing an old version of Nginx with the chunkin module [2] compiled in, or ## - using a newer version of Nginx. ## -## At the time of writing we do not know if either of these theoretical solutions works. +## At the time of writing we do not know if either of these theoretical solutions works. ## As a workaround users can use Git over SSH to push large files. ## ## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 @@ -42,7 +42,7 @@ server { listen *:80 default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - + ## Redirects all traffic to the HTTPS host root /nowhere; ## root doesn't have to be a valid path since we are redirecting rewrite ^ https://$server_name$request_uri? permanent; @@ -65,6 +65,7 @@ server { ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; + # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4'; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; @@ -72,7 +73,7 @@ server { ssl_prefer_server_ciphers on; - ## [WARNING] The following header states that the browser should only communicate + ## [WARNING] The following header states that the browser should only communicate ## with your server over a secure connection for the next 24 months. add_header Strict-Transport-Security max-age=63072000; add_header X-Frame-Options SAMEORIGIN; From d059f50d4c232903440dcf2adc4f26e3ffb3099f Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Fri, 10 Oct 2014 12:03:32 +0200 Subject: [PATCH 021/134] Refactor OAuth refactorings to CE --- .../omniauth_callbacks_controller.rb | 35 ++++---- lib/gitlab/ldap/user.rb | 75 +++++++++------- lib/gitlab/oauth/auth_hash.rb | 2 +- lib/gitlab/oauth/user.rb | 79 +++++++++-------- spec/lib/gitlab/ldap/user_spec.rb | 20 ++--- spec/lib/gitlab/oauth/auth_hash_spec.rb | 55 ++++++++++++ spec/lib/gitlab/oauth/user_spec.rb | 88 +++++++------------ 7 files changed, 191 insertions(+), 163 deletions(-) create mode 100644 spec/lib/gitlab/oauth/auth_hash_spec.rb diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 3ed6a69c2d..fa5685938f 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -15,15 +15,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController error.to_s.humanize if error end + # We only find ourselves here + # if the authentication to LDAP was successful. def ldap - # We only find ourselves here - # if the authentication to LDAP was successful. - @user = Gitlab::LDAP::User.find_or_create(oauth) - @user.remember_me = true if @user.persisted? + @user = Gitlab::LDAP::User.new(oauth) + @user.save if @user.changed? # will also save new users + gl_user = @user.gl_user + gl_user.remember_me = true if @user.persisted? # Do additional LDAP checks for the user filter and EE features - if Gitlab::LDAP::Access.allowed?(@user) - sign_in_and_redirect(@user) + if Gitlab::LDAP::Access.allowed?(gl_user) + sign_in_and_redirect(gl_user) else flash[:alert] = "Access denied for your LDAP account." redirect_to new_user_session_path @@ -46,24 +48,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController current_user.save redirect_to profile_path else - @user = Gitlab::OAuth::User.find(oauth) + @user = Gitlab::OAuth::User.new(oauth) - # Create user if does not exist - # and allow_single_sign_on is true - if Gitlab.config.omniauth['allow_single_sign_on'] && !@user - @user, errors = Gitlab::OAuth::User.create(oauth) + if Gitlab.config.omniauth['allow_single_sign_on'] && @user.new? + @user.save end - if @user && !errors - sign_in_and_redirect(@user) + if @user.valid? + sign_in_and_redirect(@user.gl_user) else - if errors - error_message = errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") - redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return - else - flash[:notice] = "There's no such user!" - end - redirect_to new_user_session_path + error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") + redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 25b5a702f9..006ef17072 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -10,22 +10,6 @@ module Gitlab module LDAP class User < Gitlab::OAuth::User class << self - def find_or_create(auth_hash) - self.auth_hash = auth_hash - find(auth_hash) || find_and_connect_by_email(auth_hash) || create(auth_hash) - end - - def find_and_connect_by_email(auth_hash) - self.auth_hash = auth_hash - user = model.find_by(email: self.auth_hash.email) - - if user - user.update_attributes(extern_uid: auth_hash.uid, provider: auth_hash.provider) - Gitlab::AppLogger.info("(LDAP) Updating legacy LDAP user #{self.auth_hash.email} with extern_uid => #{auth_hash.uid}") - return user - end - end - def authenticate(login, password) # Check user against LDAP backend if user is not authenticated # Only check with valid login and password to prevent anonymous bind results @@ -44,10 +28,18 @@ module Gitlab @adapter ||= OmniAuth::LDAP::Adaptor.new(ldap_conf) end - protected + def user_filter(login) + filter = Net::LDAP::Filter.eq(adapter.uid, login) + # Apply LDAP user filter if present + if ldap_conf['user_filter'].present? + user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter']) + filter = Net::LDAP::Filter.join(filter, user_filter) + end + filter + end - def find_by_uid_and_provider - find_by_uid(auth_hash.uid) + def ldap_conf + Gitlab.config.ldap end def find_by_uid(uid) @@ -58,24 +50,39 @@ module Gitlab def provider 'ldap' end + end - def raise_error(message) - raise OmniAuth::Error, "(LDAP) " + message - end + def initialize(auth_hash) + super + update_user_attributes + end - def ldap_conf - Gitlab.config.ldap - end + # instance methods + def gl_user + @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user + end - def user_filter(login) - filter = Net::LDAP::Filter.eq(adapter.uid, login) - # Apply LDAP user filter if present - if ldap_conf['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter']) - filter = Net::LDAP::Filter.join(filter, user_filter) - end - filter - end + def find_by_uid_and_provider + # LDAP distinguished name is case-insensitive + model. + where(provider: auth_hash.provider). + where('lower(extern_uid) = ?', auth_hash.uid.downcase).last + end + + def find_by_email + model.find_by(email: auth_hash.email) + end + + def update_user_attributes + gl_user.attributes = { + extern_uid: auth_hash.uid, + provider: auth_hash.provider, + email: auth_hash.email + } + end + + def changed? + gl_user.changed? end def needs_blocking? diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/oauth/auth_hash.rb index 0198f61f42..ce52beec78 100644 --- a/lib/gitlab/oauth/auth_hash.rb +++ b/lib/gitlab/oauth/auth_hash.rb @@ -21,7 +21,7 @@ module Gitlab end def name - (info.name || full_name).to_s.force_encoding('utf-8') + (info.try(:name) || full_name).to_s.force_encoding('utf-8') end def full_name diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index b768eda185..699258baee 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -6,55 +6,52 @@ module Gitlab module OAuth class User - class << self - attr_reader :auth_hash - - def find(auth_hash) - self.auth_hash = auth_hash - find_by_uid_and_provider - end - - def create(auth_hash) - user = new(auth_hash) - user.save_and_trigger_callbacks - end - - def model - ::User - end - - def auth_hash=(auth_hash) - @auth_hash = AuthHash.new(auth_hash) - end - - protected - def find_by_uid_and_provider - model.where(provider: auth_hash.provider, extern_uid: auth_hash.uid).last - end - end - - # Instance methods - attr_accessor :auth_hash, :user + attr_accessor :auth_hash, :gl_user def initialize(auth_hash) self.auth_hash = auth_hash - self.user = self.class.model.new(user_attributes) - user.skip_confirmation! end + def persisted? + gl_user.persisted? + end + + def new? + !gl_user.persisted? + end + + def valid? + gl_user.valid? + end + + def save + gl_user.save! + log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + gl_user.block if needs_blocking? + + gl_user + rescue ActiveRecord::RecordInvalid => e + log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" + return self, e.record.errors + end + + def gl_user + @user ||= find_by_uid_and_provider || build_new_user + end + + protected def auth_hash=(auth_hash) @auth_hash = AuthHash.new(auth_hash) end - def save_and_trigger_callbacks - user.save! - log.info "(OAuth) Creating user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" - user.block if needs_blocking? + def find_by_uid_and_provider + model.where(provider: auth_hash.provider, extern_uid: auth_hash.uid).last + end - user - rescue ActiveRecord::RecordInvalid => e - log.info "(OAuth) Email #{e.record.errors[:email]}. Username #{e.record.errors[:username]}" - return nil, e.record.errors + def build_new_user + model.new(user_attributes).tap do |user| + user.skip_confirmation! + end end def user_attributes @@ -80,6 +77,10 @@ module Gitlab def needs_blocking? Gitlab.config.omniauth['block_auto_created_users'] end + + def model + ::User + end end end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index d232cb2075..a1aec0bb96 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -1,30 +1,28 @@ require 'spec_helper' describe Gitlab::LDAP::User do - let(:gl_user) { Gitlab::LDAP::User } + let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) } let(:info) do - double( + { name: 'John', email: 'john@example.com', nickname: 'john' - ) + } + end + let(:auth_hash) do + double(uid: 'my-uid', provider: 'ldap', info: double(info)) end - before { Gitlab.config.stub(omniauth: {}) } describe :find_or_create do - let(:auth) do - double(info: info, provider: 'ldap', uid: 'my-uid') - end - it "finds the user if already existing" do existing_user = create(:user, extern_uid: 'my-uid', provider: 'ldap') - expect{ gl_user.find_or_create(auth) }.to_not change{ User.count } + expect{ gl_user.save }.to_not change{ User.count } end it "connects to existing non-ldap user if the email matches" do existing_user = create(:user, email: 'john@example.com') - expect{ gl_user.find_or_create(auth) }.to_not change{ User.count } + expect{ gl_user.save }.to_not change{ User.count } existing_user.reload expect(existing_user.extern_uid).to eql 'my-uid' @@ -32,7 +30,7 @@ describe Gitlab::LDAP::User do end it "creates a new user if not found" do - expect{ gl_user.find_or_create(auth) }.to change{ User.count }.by(1) + expect{ gl_user.save }.to change{ User.count }.by(1) end end diff --git a/spec/lib/gitlab/oauth/auth_hash_spec.rb b/spec/lib/gitlab/oauth/auth_hash_spec.rb new file mode 100644 index 0000000000..5eb77b492b --- /dev/null +++ b/spec/lib/gitlab/oauth/auth_hash_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::OAuth::AuthHash do + let(:auth_hash) do + Gitlab::OAuth::AuthHash.new(double({ + provider: 'twitter', + uid: uid, + info: double(info_hash) + })) + end + let(:uid) { 'my-uid' } + let(:email) { 'my-email@example.com' } + let(:nickname) { 'my-nickname' } + let(:info_hash) { + { + email: email, + nickname: nickname, + name: 'John', + first_name: "John", + last_name: "Who" + } + } + + context "defaults" do + it { expect(auth_hash.provider).to eql 'twitter' } + it { expect(auth_hash.uid).to eql uid } + it { expect(auth_hash.email).to eql email } + it { expect(auth_hash.username).to eql nickname } + it { expect(auth_hash.name).to eql "John" } + it { expect(auth_hash.password).to_not be_empty } + end + + context "email not provided" do + before { info_hash.delete(:email) } + it "generates a temp email" do + expect( auth_hash.email).to start_with('temp-email-for-oauth') + end + end + + context "username not provided" do + before { info_hash.delete(:nickname) } + + it "takes the first part of the email as username" do + expect( auth_hash.username ).to eql "my-email" + end + end + + context "name not provided" do + before { info_hash.delete(:name) } + + it "concats first and lastname as the name" do + expect( auth_hash.name ).to eql "John Who" + end + end +end \ No newline at end of file diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index c241e19860..e4e96fd9f4 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -1,83 +1,55 @@ require 'spec_helper' describe Gitlab::OAuth::User do - let(:gl_auth) { Gitlab::OAuth::User } - let(:info) do - double( + let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) } + let(:gl_user) { oauth_user.gl_user } + let(:uid) { 'my-uid' } + let(:provider) { 'my-provider' } + let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } + let(:info_hash) do + { nickname: 'john', name: 'John', email: 'john@mail.com' - ) + } end - before do - Gitlab.config.stub(omniauth: {}) - end - - describe :find do + describe :persisted? do let!(:existing_user) { create(:user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') - assert gl_auth.find(auth) + expect( oauth_user.persisted? ).to be_true end - it "finds an existing user based on nested uid and provider" do - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') - assert gl_auth.find(auth) + it "returns false if use is not found in database" do + auth_hash.stub(uid: 'non-existing') + expect( oauth_user.persisted? ).to be_false end end - describe :create do - it "should create user from LDAP" do - auth = double(info: info, uid: 'my-uid', provider: 'ldap') - user = gl_auth.create(auth) + describe :save do + context "LDAP" do + let(:provider) { 'ldap' } + it "creates a user from LDAP" do + oauth_user.save - user.should be_valid - user.extern_uid.should == auth.uid - user.provider.should == 'ldap' + expect(gl_user).to be_valid + expect(gl_user.extern_uid).to eql uid + expect(gl_user.provider).to eql 'ldap' + end end - it "should create user from Omniauth" do - auth = double(info: info, uid: 'my-uid', provider: 'twitter') - user = gl_auth.create(auth) + context "twitter" do + let(:provider) { 'twitter' } - user.should be_valid - user.extern_uid.should == auth.uid - user.provider.should == 'twitter' - end + it "creates a user from Omniauth" do + oauth_user.save - it "should apply defaults to user" do - auth = double(info: info, uid: 'my-uid', provider: 'ldap') - user = gl_auth.create(auth) - - user.should be_valid - user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit - user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group - end - - it "Set a temp email address if not provided (like twitter does)" do - info = double( - uid: 'my-uid', - nickname: 'john', - name: 'John' - ) - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') - - user = gl_auth.create(auth) - expect(user.email).to_not be_empty - end - - it 'generates a username if non provided (google)' do - info = double( - uid: 'my-uid', - name: 'John', - email: 'john@example.com' - ) - auth = double(info: info, uid: 'my-uid', provider: 'my-provider') - - user = gl_auth.create(auth) - expect(user.username).to eql 'john' + expect(gl_user).to be_valid + expect(gl_user.extern_uid).to eql uid + expect(gl_user.provider).to eql 'twitter' + end end end end From 4149fc24cfe4ffa2d0d950e7f930529a05899b7c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 12:51:00 +0200 Subject: [PATCH 022/134] Bump html-pipeline-gitlab gem version --- Gemfile.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index babb23ed60..517466f3d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,9 +241,11 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.0) - gitlab_emoji (~> 0.0.1.1) + html-pipeline-gitlab (0.1.4) + actionpack (~> 4) + gitlab_emoji (~> 0.0.1) html-pipeline (~> 1.11.0) + sanitize (~> 2.1) http_parser.rb (0.5.3) httparty (0.13.0) json (~> 1.8) From 0189be0831350a5d473884a5b454a10509ff58ce Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 15:39:48 +0300 Subject: [PATCH 023/134] Use short_id instead of [0..N] for short version of commit sha Signed-off-by: Dmitriy Zaporozhets --- app/helpers/commits_helper.rb | 4 ++++ app/models/commit.rb | 6 +++++- app/models/event.rb | 2 +- app/models/merge_request_diff.rb | 2 +- app/views/events/_commit.html.haml | 2 +- app/views/events/_event_push.atom.haml | 2 +- app/views/events/event/_push.html.haml | 2 +- app/views/projects/commit/_commit_box.html.haml | 2 +- app/views/projects/tree/_submodule_item.html.haml | 4 ++-- app/views/projects/wikis/history.html.haml | 2 +- app/views/search/results/_note.html.haml | 2 +- features/steps/project/commits/commits.rb | 2 +- spec/models/commit_spec.rb | 2 +- spec/models/note_spec.rb | 4 ++-- spec/support/mentionable_shared_examples.rb | 11 +++++------ 15 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index cab2984a4c..0e0532b65b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -120,4 +120,8 @@ module CommitsHelper class: 'commit-short-id') end end + + def truncate_sha(sha) + Commit.truncate_sha(sha) + end end diff --git a/app/models/commit.rb b/app/models/commit.rb index a1343b65c7..61551df9e2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -26,6 +26,10 @@ class Commit def diff_line_count(diffs) diffs.reduce(0) { |sum, d| sum + d.diff.lines.count } end + + def truncate_sha(sha) + sha[0..10] + end end attr_accessor :raw @@ -111,7 +115,7 @@ class Commit # Mentionable override. def gfm_reference - "commit #{sha[0..5]}" + "commit #{short_id}" end def method_missing(m, *args, &block) diff --git a/app/models/event.rb b/app/models/event.rb index 9e296c0028..c0b126713a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -266,7 +266,7 @@ class Event < ActiveRecord::Base end def note_short_commit_id - note_commit_id[0..8] + Commit.truncate_sha(note_commit_id) end def note_commit? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 409e82ed1e..a71122d5e0 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -55,7 +55,7 @@ class MergeRequestDiff < ActiveRecord::Base end def last_commit_short_sha - @last_commit_short_sha ||= last_commit.sha[0..10] + @last_commit_short_sha ||= last_commit.short_id end private diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index 0e03e116e7..f0c34def14 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,5 +1,5 @@ %li.commit .commit-row-title - = link_to commit[:id][0..8], project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' + = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit_short_id", alt: ''   = gfm event_commit_title(commit[:message]), project diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 17228c430c..2b63519eda 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -2,7 +2,7 @@ - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] - = link_to "(##{commit[:id][0...8]})", project_commit_path(event.project, id: commit[:id]) + = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) %i at = commit[:timestamp].to_time.to_s(:short) diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 1bca64c7d5..b912b5e092 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -22,4 +22,4 @@ - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. = link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do - %strong Compare → #{event.commit_from[0..7]}...#{event.commit_to[0..7]} + %strong Compare → #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)} diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0b6b6af4f9..e149f017f8 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -35,7 +35,7 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.id[0...10], project_commit_path(@project, parent) + = link_to parent.short_id, project_commit_path(@project, parent) - if @branches.any? .commit-info-row diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index a8ec9df2c8..46e9be4af8 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -7,8 +7,8 @@ @ %span.monospace - if commit.nil? - #{submodule_item.id[0..10]} + #{truncate_sha(submodule_item.id)} - else - = link_to "#{submodule_item.id[0..10]}", commit + = link_to "#{truncate_sha(submodule_item.id)}", commit %td %td.hidden-xs diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index d3a66c48c9..ef4b8f7471 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -17,7 +17,7 @@ %tr %td = link_to project_wiki_path(@project, @page, version_id: commit.id) do - = commit.id[0..10] + = truncate_sha(commit.id) %td = commit.author.name %td diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index f2327cd69c..a44a4542df 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -10,7 +10,7 @@ = project.name_with_namespace · = link_to project_commit_path(project, note.commit_id, anchor: dom_id(note)) do - Commit #{note.commit_id[0..8]} + Commit #{truncate_sha(note.commit_id)} - else = link_to project do = project.name_with_namespace diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index c054e0e828..935f313e29 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps commit = @project.repository.commit page.should have_content(@project.name) page.should have_content(commit.message[0..20]) - page.should have_content(commit.id.to_s[0..5]) + page.should have_content(commit.short_id) end step 'I click atom feed link' do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 6f201adc4e..24bbf4f57d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -75,7 +75,7 @@ eos it_behaves_like 'a mentionable' do let(:subject) { commit } let(:mauthor) { create :user, email: commit.author_email } - let(:backref_text) { "commit #{subject.sha[0..5]}" } + let(:backref_text) { "commit #{subject.short_id}" } let(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } } # Include the subject in the repository stub. diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index eeecd714a2..d8b4a27eb0 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -228,7 +228,7 @@ describe Note do it { should be_valid } its(:noteable) { should == issue } - its(:note) { should == "_mentioned in commit #{commit.sha[0..5]}_" } + its(:note) { should == "_mentioned in commit #{commit.sha[0..10]}_" } end context 'merge request from an issue' do @@ -267,7 +267,7 @@ describe Note do its(:noteable_type) { should == "Commit" } its(:noteable_id) { should be_nil } its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in commit #{parent_commit.id[0...6]}_" } + its(:note) { should == "_mentioned in commit #{parent_commit.id[0..10]}_" } end end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 692834c9f2..ebd7420669 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -30,15 +30,15 @@ def common_mentionable_setup "!#{mentioned_mr.iid}, " + "#{ext_proj.path_with_namespace}##{ext_issue.iid}, " + "#{ext_proj.path_with_namespace}!#{ext_mr.iid}, " + - "#{ext_proj.path_with_namespace}@#{ext_commit.id[0..5]}, " + - "#{mentioned_commit.sha[0..5]} and itself as #{backref_text}" + "#{ext_proj.path_with_namespace}@#{ext_commit.short_id}, " + + "#{mentioned_commit.sha[0..10]} and itself as #{backref_text}" end before do # Wire the project's repository to return the mentioned commit, and +nil+ for any # unrecognized commits. - commitmap = { '123456' => mentioned_commit } - extra_commits.each { |c| commitmap[c.sha[0..5]] = c } + commitmap = { '1234567890a' => mentioned_commit } + extra_commits.each { |c| commitmap[c.short_id] = c } mproject.repository.stub(:commit) { |sha| commitmap[sha] } set_mentionable_text.call(ref_string) end @@ -54,7 +54,6 @@ shared_examples 'a mentionable' do it "extracts references from its reference property" do # De-duplicate and omit itself refs = subject.references(mproject) - refs.should have(6).items refs.should include(mentioned_issue) refs.should include(mentioned_mr) @@ -90,7 +89,7 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do new_text = "still mentions ##{mentioned_issue.iid}, " + - "#{mentioned_commit.sha[0..5]}, " + + "#{mentioned_commit.sha[0..10]}, " + "#{ext_issue.iid}, " + "new refs: ##{other_issue.iid}, " + "#{ext_proj.path_with_namespace}##{other_ext_issue.iid}" From 200118357de47c6db69a48eac2b488bfb46e9026 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 15:44:27 +0300 Subject: [PATCH 024/134] Use full commit sha width for reference in note body to prevent Ambiguous SHA1 prefix problem Signed-off-by: Dmitriy Zaporozhets --- app/models/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 61551df9e2..c30a630429 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -115,7 +115,7 @@ class Commit # Mentionable override. def gfm_reference - "commit #{short_id}" + "commit #{id}" end def method_missing(m, *args, &block) From daa55f31d899819069ddbaa9596769a233b5a729 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 15:58:11 +0300 Subject: [PATCH 025/134] Dont raise exception when wrong commit id passed Signed-off-by: Dmitriy Zaporozhets --- app/models/repository.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/repository.rb b/app/models/repository.rb index 339e485e6d..93994123a9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -30,6 +30,8 @@ class Repository commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Commit.new(commit) if commit commit + rescue Rugged::OdbError => ex + nil end def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) From f7342ce56764aaf6465bca74239955778c25107b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 16:17:00 +0300 Subject: [PATCH 026/134] Add more stuff to changelog Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7e6e0f5f64..c98d21f986 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,12 @@ v 7.4.0 - Add select field type for services options (Sullivan Senechal) - Add cross-project references to the Markdown parser (Vinnie Okada) - Add task lists to issue and merge request descriptions (Vinnie Okada) + - Snippets can be public, internal or private + - Improve danger zone: ask project path to confirm data-loss action + - Raise exception on forgery + - Show build coverage in Merge Requests (requires GitLab CI v5.1) + - New milestone and label links on issue edit form + - Improved repository graphs v 7.3.2 - Fix creating new file via web editor From ea04ed7879ad7177bef7a6dbe3bf90d76ebb8b45 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 16:30:14 +0300 Subject: [PATCH 027/134] Use 8chars short sha for commit in views Signed-off-by: Dmitriy Zaporozhets --- app/models/commit.rb | 8 +++++++- app/views/projects/blame/show.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 2 +- app/views/projects/commits/_inline_commit.html.haml | 2 +- spec/models/commit_spec.rb | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index c30a630429..cbe0a39bc7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -27,8 +27,9 @@ class Commit diffs.reduce(0) { |sum, d| sum + d.diff.lines.count } end + # Truncate sha to 8 characters def truncate_sha(sha) - sha[0..10] + sha[0..7] end end @@ -128,6 +129,11 @@ class Commit super end + # Truncate sha to 8 characters + def short_id + @raw.short_id(7) + end + def parents @parents ||= Commit.decorate(super) end diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index e5cde488c3..bdf02c6285 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,7 +15,7 @@ %tr %td.blame-commit %span.commit - = link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(@project, commit), class: "commit_short_id"   = commit_author_link(commit, avatar: true, size: 16)   diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 68852ba973..1eb17f760d 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,6 +1,6 @@ %li.commit.js-toggle-container .commit-row-title - = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id"   %span.str-truncated = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index b36369b428..574599aa2d 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -1,6 +1,6 @@ %li.commit.inline-commit .commit-row-title - = link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id"   %span.str-truncated = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 24bbf4f57d..a6ec44da4b 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -75,7 +75,7 @@ eos it_behaves_like 'a mentionable' do let(:subject) { commit } let(:mauthor) { create :user, email: commit.author_email } - let(:backref_text) { "commit #{subject.short_id}" } + let(:backref_text) { "commit #{subject.id}" } let(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } } # Include the subject in the repository stub. From 0852d5e480e2789dcc6b5cce08fc0875f97af4bf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 16:36:06 +0300 Subject: [PATCH 028/134] Fix tests Signed-off-by: Dmitriy Zaporozhets --- spec/models/note_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index d8b4a27eb0..2d839e9611 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -228,7 +228,7 @@ describe Note do it { should be_valid } its(:noteable) { should == issue } - its(:note) { should == "_mentioned in commit #{commit.sha[0..10]}_" } + its(:note) { should == "_mentioned in commit #{commit.sha}_" } end context 'merge request from an issue' do @@ -267,7 +267,7 @@ describe Note do its(:noteable_type) { should == "Commit" } its(:noteable_id) { should be_nil } its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in commit #{parent_commit.id[0..10]}_" } + its(:note) { should == "_mentioned in commit #{parent_commit.id}_" } end end From 8c01448cf9ffb3662ebd22e02077e48ba59c65ca Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 17:39:29 +0300 Subject: [PATCH 029/134] Dontr decoarate already decorated stuff Signed-off-by: Dmitriy Zaporozhets --- app/models/commit.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index cbe0a39bc7..212229649f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -19,7 +19,13 @@ class Commit class << self def decorate(commits) - commits.map { |c| self.new(c) } + commits.map do |commit| + if commit.kind_of?(Commit) + commit + else + self.new(commit) + end + end end # Calculate number of lines to render for diffs From 2ea166fc338f95cb9f6db1c61426dce4b2cfd8e1 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 10 Oct 2014 17:31:47 +0200 Subject: [PATCH 030/134] Make sure relative url and asset_host are honored, specs. --- Gemfile.lock | 2 +- lib/gitlab/markdown.rb | 7 ++++++- spec/helpers/gitlab_markdown_helper_spec.rb | 14 +++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 517466f3d1..a9b71fec13 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,7 +241,7 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.4) + html-pipeline-gitlab (0.1.5) actionpack (~> 4) gitlab_emoji (~> 0.0.1) html-pipeline (~> 1.11.0) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index d3e9bafb06..ddcce7557a 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -78,9 +78,14 @@ module Gitlab HTML::Pipeline::Gitlab::GitlabEmojiFilter ] + markdown_context = { + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host + } + markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline - result = markdown_pipeline.call(text) + result = markdown_pipeline.call(text, markdown_context) text = result[:output].to_html(save_with: 0) allowed_attributes = ActionView::Base.sanitized_allowed_attributes diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 15033f0743..26908abc30 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -576,9 +576,21 @@ describe GitlabMarkdownHelper do end it "should generate absolute urls for emoji" do - markdown(":smile:").should include("src=\"#{url_helper('emoji/smile')}") + markdown(":smile:").should include("src=\"http://localhost/assets/emoji/smile.png") end + it "should generate absolute urls for emoji if relative url is present" do + Gitlab.config.gitlab.stub(:url).and_return('http://localhost/gitlab/root') + markdown(":smile:").should include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") + end + + it "should generate absolute urls for emoji if asset_host is present" do + Gitlab::Application.config.stub(:asset_host).and_return("https://cdn.example.com") + ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") + markdown(":smile:").should include("src=\"https://cdn.example.com/assets/emoji/smile.png") + end + + it "should handle relative urls for a file in master" do actual = "[GitLab API doc](doc/api/README.md)\n" expected = "

    GitLab API doc

    \n" From b2d1e97df99dfdda65d5411de76dd34091d6be3e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Oct 2014 18:42:05 +0300 Subject: [PATCH 031/134] Fix spinach tests Signed-off-by: Dmitriy Zaporozhets --- features/steps/project/merge_requests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index c009568977..fae0cec53a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -111,7 +111,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I click on the commit in the merge request' do within '.mr-commits' do - click_link sample_commit.id[0..8] + click_link Commit.truncate_sha(sample_commit.id) end end From 8a52ff9c293a46fa7d6b4427f5f25992c7dc2c60 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 11 Oct 2014 12:53:27 -0500 Subject: [PATCH 032/134] Document Markdown table formatting issue Add a note to the Markdown documentation about a quirk of Redcarpet's table parsing. --- doc/markdown/markdown.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 6d96da76ad..edb7a97550 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -510,6 +510,10 @@ Code above produces next output: | cell 1 | cell 2 | | cell 3 | cell 4 | +**Note** + +The row of dashes between the table header and body must have at least three dashes in each column. + ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). From ab64ca7a11becad2ca32fdb7ef0530437aa361d9 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 12 Oct 2014 05:23:12 -0700 Subject: [PATCH 033/134] improve wording on protected branches page --- app/views/projects/protected_branches/index.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 3980a6c086..ace67724ab 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,13 +1,13 @@ %h3.page-title Protected branches -%p.light This ability keeps stable branches secure and forces developers to use code reviews +%p.light Keep stable branches secure and force developers to use Merge Requests %hr .bs-callout.bs-callout-info %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} - %li prevents anyone from force pushing to the branch - %li prevents anyone from deleting the branch + %li prevent anyone from force pushing to the branch + %li prevent anyone from deleting the branch %p Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} - if can? current_user, :admin_project, @project From b02c21df5cc0dcc795f71f8c11d05d61dc2ad897 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 12 Oct 2014 21:49:20 +0300 Subject: [PATCH 034/134] Fix tests Signed-off-by: Dmitriy Zaporozhets --- spec/helpers/gitlab_markdown_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 15033f0743..246bb535fc 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -60,7 +60,7 @@ describe GitlabMarkdownHelper do end it "should link using a short id" do - actual = "Backported from #{commit.short_id(6)}" + actual = "Backported from #{commit.short_id}" gfm(actual).should match(expected) end From 5b2a42a091b2300ae1962b158b1496ac160c9e0f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 11 Oct 2014 21:17:02 -0500 Subject: [PATCH 035/134] Preserve link href in truncated note view Notes on the dashboard views are truncated to 150 characters; this change ensures that when a link's text is truncated it still points to the correct URL. --- app/helpers/events_helper.rb | 5 ++-- app/helpers/gitlab_markdown_helper.rb | 43 ++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6aeab7bb8c..100dde1027 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -136,9 +136,8 @@ module EventsHelper end def event_note(text) - text = first_line_in_markdown(text) - text = truncate(text, length: 150) - sanitize(markdown(text), tags: %w(a img b pre p)) + text = first_line_in_markdown(text, 150) + sanitize(text, tags: %w(a img b pre p)) end def event_commit_title(message) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0365681a12..27d8aee830 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -51,12 +51,21 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end - def first_line_in_markdown(text) - line = text.split("\n").detect do |i| + # Return the first line of +text+, up to +max_chars+, after parsing the line + # as Markdown. HTML tags in the parsed output are not counted toward the + # +max_chars+ limit. If the length limit falls within a tag's contents, then + # the tag contents are truncated without removing the closing tag. + def first_line_in_markdown(text, max_chars = nil) + line = text.split("\n").find do |i| i.present? && markdown(i).present? end - line += '...' unless line.nil? - line + + if line + md = markdown(line) + truncated = truncate_visible(md, max_chars || md.length) + end + + truncated end def render_wiki_content(wiki_page) @@ -204,4 +213,30 @@ module GitlabMarkdownHelper def correct_ref @ref ? @ref : "master" end + + private + + # Return +text+, truncated to +max_chars+ characters, excluding any HTML + # tags. + def truncate_visible(text, max_chars) + doc = Nokogiri::HTML.fragment(text) + content_length = 0 + + doc.traverse do |node| + if node.text? || node.content.empty? + if content_length >= max_chars + node.remove + next + end + + num_remaining = max_chars - content_length + if node.content.length > num_remaining + node.content = node.content.truncate(num_remaining) + end + content_length += node.content.length + end + end + + doc.to_html + end end From d94be1ddbfb8fadf015eacbdc58c62c3cc0ffa91 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 12 Oct 2014 14:29:42 -0700 Subject: [PATCH 036/134] Cleanup MySQL database Addresses changes made to installation guide and config files but never applied in update process. Relevant changes to installation guide and config files were made in gitlabhq@cbb5b00, gitlabhq@498a4e6, gitlabhq@c33d5e1, gitlabhq@485162e#diff-e1059d0fa0437ffad94facff86210603, gitlabhq@72e2fe2#diff-d1b4ff7de834bae6008dd49550413a6f, gitlabhq@5163a8f#diff-e1059d0fa0437ffad94facff86210603, gitlabhq@993af5d#diff-e1059d0fa0437ffad94facff86210603, & gitlabhq@d3f5a0c. --- doc/update/7.3-to-7.4.md | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 doc/update/7.3-to-7.4.md diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md new file mode 100644 index 0000000000..0ad29e5207 --- /dev/null +++ b/doc/update/7.3-to-7.4.md @@ -0,0 +1,68 @@ +# From 7.3 to 7.4 + +## GitLab 7.4 has not been released yet! + +This document currently just serves as a place to keep track of updates that will be needed for the 7.4 update. + +## Update config files + +* Add `collation: utf8_general_ci` to config/database.yml as seen in [config/database.yml.mysql](config/database.yml.mysql) + +## Optional optimizations for GitLab setups with MySQL databases + +Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure take a current MySQL database backup beforehand. + +``` +# Secure your MySQL installation (added in GitLab 6.2) +sudo mysql_secure_installation + +# Login to MySQL +mysql -u root -p + +# do not type the 'mysql>', this is part of the prompt + +# Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# Find MySQL users +mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; + +# If git user exists and gitlab user does not exist +# you are done with the database cleanup tasks +mysql> \q + +# If both users exist skip to Delete gitlab user + +# Create new user for GitLab (changed in GitLab 6.4) +# change $password in the command below to a real password you pick +mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password'; + +# Grant the git user necessary permissions on the database +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; + +# Delete the old gitlab user +mysql> DELETE FROM mysql.user WHERE user='gitlab'; + +# Quit the database session +mysql> \q + +# Try connecting to the new database with the new user +sudo -u git -H mysql -u git -p -D gitlabhq_production + +# Type the password you replaced $password with earlier + +# You should now see a 'mysql>' prompt + +# Quit the database session +mysql> \q + +# Update database configuration details +# See config/database.yml.mysql for latest recommended configuration details +# Remove the reaping_frequency setting line if it exists (removed in GitLab 6.8) +# Set production -> pool: 10 (updated in GitLab 5.3 & 6.2) +# Set production -> username: git +# Set production -> password: the password your replaced $password with earlier +sudo -u git -H editor /home/git/gitlab/config/database.yml +``` \ No newline at end of file From 2b3090d91b6d508cb88feab9c4d32791566bab63 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 12 Oct 2014 14:39:41 -0700 Subject: [PATCH 037/134] simplify schema.rb reset in upgrade guides --- doc/update/6.x-or-7.x-to-7.3.md | 3 +-- doc/update/7.2-to-7.3.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/update/6.x-or-7.x-to-7.3.md b/doc/update/6.x-or-7.x-to-7.3.md index 171fcb4033..fe3530ef9c 100644 --- a/doc/update/6.x-or-7.x-to-7.3.md +++ b/doc/update/6.x-or-7.x-to-7.3.md @@ -64,12 +64,12 @@ sudo gem install bundler --no-ri --no-rdoc ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` For GitLab Community Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable ``` @@ -78,7 +78,6 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable-ee ``` diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index 329b763322..44f3f8f1a3 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -18,12 +18,12 @@ sudo service gitlab stop ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` For GitLab Community Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable ``` @@ -32,7 +32,6 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically sudo -u git -H git checkout 7-3-stable-ee ``` From b3c70d001d7371e8952cd7be879e727b5ee4155a Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 12 Oct 2014 23:07:18 -0500 Subject: [PATCH 038/134] Improve dashboard note view and add tests Update the `#first_line_in_markdown` method so that the first line of parsed text is displayed more reliably, and the continuation indicators ("...") are displayed in all cases where the note is truncated. Also add Rspec tests for `EventsHelper#event_note`. --- app/helpers/events_helper.rb | 2 +- app/helpers/gitlab_markdown_helper.rb | 35 ++++++++++++------ spec/helpers/events_helper_spec.rb | 52 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 spec/helpers/events_helper_spec.rb diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 100dde1027..71f97fbb8c 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -137,7 +137,7 @@ module EventsHelper def event_note(text) text = first_line_in_markdown(text, 150) - sanitize(text, tags: %w(a img b pre p)) + sanitize(text, tags: %w(a img b pre code p)) end def event_commit_title(message) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 27d8aee830..7d3cb74982 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -56,16 +56,9 @@ module GitlabMarkdownHelper # +max_chars+ limit. If the length limit falls within a tag's contents, then # the tag contents are truncated without removing the closing tag. def first_line_in_markdown(text, max_chars = nil) - line = text.split("\n").find do |i| - i.present? && markdown(i).present? - end + md = markdown(text).strip - if line - md = markdown(line) - truncated = truncate_visible(md, max_chars || md.length) - end - - truncated + truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) @@ -221,22 +214,44 @@ module GitlabMarkdownHelper def truncate_visible(text, max_chars) doc = Nokogiri::HTML.fragment(text) content_length = 0 + truncated = false doc.traverse do |node| if node.text? || node.content.empty? - if content_length >= max_chars + if truncated node.remove next end + # Handle line breaks within a node + if node.content.strip.lines.length > 1 + node.content = "#{node.content.lines.first.chomp}..." + truncated = true + end + num_remaining = max_chars - content_length if node.content.length > num_remaining node.content = node.content.truncate(num_remaining) + truncated = true end content_length += node.content.length end + + truncated = truncate_if_block(node, truncated) end doc.to_html end + + # Used by #truncate_visible. If +node+ is the first block element, and the + # text hasn't already been truncated, then append "..." to the node contents + # and return true. Otherwise return false. + def truncate_if_block(node, truncated) + if node.element? && node.description.block? && !truncated + node.content = "#{node.content}..." if node.next_sibling + true + else + truncated + end + end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb new file mode 100644 index 0000000000..4de54d291f --- /dev/null +++ b/spec/helpers/events_helper_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe EventsHelper do + include ApplicationHelper + include GitlabMarkdownHelper + + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(event_note(input)).to match(input) + expect(event_note(input)).not_to match(/\.\.\.\z/) + end + + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with inline code' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' + + expect(event_note(input)).to match(expected) + end + + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = '
    Code block...
    ' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = "#{text}#{text}#{text}#{text}" # 200 chars + expected = "#{text}#{text}".sub(/.{3}/, '...') + + expect(event_note(input)).to match(expected) + end + + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...' + + expect(event_note(input)).to match(link_url) + expect(event_note(input)).to match(expected_link_text) + end +end From b0e92ca9ae9a2c051381b9cd3817123f6907e4fa Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Mon, 13 Oct 2014 00:52:08 -0700 Subject: [PATCH 039/134] minor updates to mysql cleanup * take -> make * correct incorrect details about when pool size was changed --- doc/update/7.3-to-7.4.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index 0ad29e5207..2e1b993aeb 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -10,7 +10,7 @@ This document currently just serves as a place to keep track of updates that wil ## Optional optimizations for GitLab setups with MySQL databases -Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure take a current MySQL database backup beforehand. +Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure make a current MySQL database backup beforehand. ``` # Secure your MySQL installation (added in GitLab 6.2) @@ -61,8 +61,8 @@ mysql> \q # Update database configuration details # See config/database.yml.mysql for latest recommended configuration details # Remove the reaping_frequency setting line if it exists (removed in GitLab 6.8) -# Set production -> pool: 10 (updated in GitLab 5.3 & 6.2) +# Set production -> pool: 10 (updated in GitLab 5.3) # Set production -> username: git # Set production -> password: the password your replaced $password with earlier sudo -u git -H editor /home/git/gitlab/config/database.yml -``` \ No newline at end of file +``` From a7e071e9822a9803e9d686484298170dade5beb5 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Mon, 13 Oct 2014 13:39:54 +0200 Subject: [PATCH 040/134] Add refactoring for multiple LDAP server support These changes are ported from EE to CE. Apply changes for app directory --- .../omniauth_callbacks_controller.rb | 39 +++++++++---------- app/controllers/sessions_controller.rb | 4 ++ app/helpers/oauth_helper.rb | 2 +- app/models/user.rb | 5 +-- app/views/devise/sessions/_new_ldap.html.haml | 2 +- app/views/devise/sessions/new.html.haml | 17 ++++---- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 3ed6a69c2d..0f364a48ea 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -15,21 +15,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController error.to_s.humanize if error end + # We only find ourselves here + # if the authentication to LDAP was successful. def ldap - # We only find ourselves here - # if the authentication to LDAP was successful. - @user = Gitlab::LDAP::User.find_or_create(oauth) - @user.remember_me = true if @user.persisted? + @user = Gitlab::LDAP::User.new(oauth) + @user.save if @user.changed? # will also save new users + gl_user = @user.gl_user + gl_user.remember_me = true if @user.persisted? # Do additional LDAP checks for the user filter and EE features - if Gitlab::LDAP::Access.allowed?(@user) - sign_in_and_redirect(@user) + if @user.allowed? + sign_in_and_redirect(gl_user) else flash[:alert] = "Access denied for your LDAP account." redirect_to new_user_session_path end end + Gitlab.config.ldap.servers.each do |server| + alias_method server.provider_name, :ldap + end + def omniauth_error @provider = params[:provider] @error = params[:error] @@ -46,24 +52,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController current_user.save redirect_to profile_path else - @user = Gitlab::OAuth::User.find(oauth) + @user = Gitlab::OAuth::User.new(oauth) - # Create user if does not exist - # and allow_single_sign_on is true - if Gitlab.config.omniauth['allow_single_sign_on'] && !@user - @user, errors = Gitlab::OAuth::User.create(oauth) + if Gitlab.config.omniauth['allow_single_sign_on'] && @user.new? + @user.save end - if @user && !errors - sign_in_and_redirect(@user) + if @user.valid? + sign_in_and_redirect(@user.gl_user) else - if errors - error_message = errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") - redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return - else - flash[:notice] = "There's no such user!" - end - redirect_to new_user_session_path + error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") + redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1bdba75c5e..e918f46bb3 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -18,6 +18,10 @@ class SessionsController < Devise::SessionsController store_location_for(:redirect, redirect_path) end + if Gitlab.config.ldap.enabled + @ldap_servers = Gitlab.config.ldap.servers + end + super end diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index c0177dacbf..7024483b8b 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -1,6 +1,6 @@ module OauthHelper def ldap_enabled? - Devise.omniauth_providers.include?(:ldap) + Gitlab.config.ldap.enabled end def default_providers diff --git a/app/models/user.rb b/app/models/user.rb index c90f246242..5abaa5495b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -178,8 +178,7 @@ class User < ActiveRecord::Base scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } - scope :ldap, -> { where(provider: 'ldap') } - + scope :ldap, -> { where('provider LIKE ?', 'ldap%') } scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # @@ -397,7 +396,7 @@ class User < ActiveRecord::Base end def ldap_user? - extern_uid && provider == 'ldap' + extern_uid && provider.start_with?('ldap') end def accessible_deploy_keys diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 6c5a878e90..0158461149 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,4 +1,4 @@ -= form_tag(user_omniauth_callback_path(:ldap), id: 'new_ldap_user' ) do += form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} %br/ diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index b70b0d6617..04e998f8be 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -4,20 +4,22 @@ .login-body - if ldap_enabled? && gitlab_config.signin_enabled %ul.nav.nav-tabs - %li.active - = link_to 'LDAP', '#tab-ldap', 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i==0)} + = link_to server['label'], "#tab-#{server.provider_name}", 'data-toggle' => 'tab' %li = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' .tab-content - %div#tab-ldap.tab-pane.active - = render partial: 'devise/sessions/new_ldap' + - @ldap_servers.each_with_index do |server,i| + %div.tab-pane{id: "tab-#{server.provider_name}", class: (:active if i==0)} + = render 'devise/sessions/new_ldap', provider: server.provider_name %div#tab-signin.tab-pane - = render partial: 'devise/sessions/new_base' + = render 'devise/sessions/new_base' - elsif ldap_enabled? - = render partial: 'devise/sessions/new_ldap' + = render 'devise/sessions/new_ldap', ldap_servers: @ldap_servers - elsif gitlab_config.signin_enabled - = render partial: 'devise/sessions/new_base' + = render 'devise/sessions/new_base' - else %div No authentication methods configured. @@ -36,7 +38,6 @@ %span.light Did not receive confirmation email? = link_to "Send again", new_confirmation_path(resource_name) - - if extra_config.has_key?('sign_in_text') %hr = markdown(extra_config.sign_in_text) From 3cd5abf635d32af0aed5f4160707ee3e10938ab6 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Mon, 13 Oct 2014 13:48:22 +0200 Subject: [PATCH 041/134] Add config changes for mutliple LDAP support (EE only) --- config/gitlab.yml.example | 103 ++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 857643c006..9302dca4ed 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -134,44 +134,66 @@ production: &base # bundle exec rake gitlab:ldap:check RAILS_ENV=production ldap: enabled: false - host: '_your_ldap_server' - port: 636 - uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' + servers: + - + ## provider_id + # + # This identifier is used by GitLab to keep track of which LDAP server each + # GitLab user belongs to. Each LDAP server known to GitLab should have a unique + # provider_id. This identifier cannot be changed once users from the LDAP server + # have started logging in to GitLab. + # + # Format: one word, using a-z (lower case) and 0-9 + # Example: 'paris' or 'uswest2' - # This setting specifies if LDAP server is Active Directory LDAP server. - # For non AD servers it skips the AD specific queries. - # If your LDAP server is not AD, set this to false. - active_directory: true + provider_id: main - # If allow_username_or_email_login is enabled, GitLab will ignore everything - # after the first '@' in the LDAP username submitted by the user on login. - # - # Example: - # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; - # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. - # - # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to - # disable this setting, because the userPrincipalName contains an '@'. - allow_username_or_email_login: false + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' - # Base where we can search for users - # - # Ex. ou=People,dc=gitlab,dc=example - # - base: '' + label: 'LDAP' - # Filter LDAP users - # - # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 - # Ex. (employeeType=developer) - # - # Note: GitLab does not support omniauth-ldap's custom filter syntax. - # - user_filter: '' + host: '_your_ldap_server' + port: 636 + uid: 'sAMAccountName' + method: 'ssl' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' ## OmniAuth settings omniauth: @@ -299,6 +321,21 @@ test: project_url: "http://redmine/projects/:issues_tracker_id" issues_url: "http://redmine/:project_id/:issues_tracker_id/:id" new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" + ldap: + enabled: false + servers: + - + provider_id: main + label: ldap + host: 127.0.0.1 + port: 3890 + uid: 'uid' + method: 'plain' # "tls" or "ssl" or "plain" + base: 'dc=example,dc=com' + user_filter: '' + group_base: 'ou=groups,dc=example,dc=com' + admin_group: '' + sync_ssh_keys: false staging: <<: *base From e1cf9c15eb38cd830a52de41b9c242add0b76767 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Mon, 13 Oct 2014 14:04:10 +0200 Subject: [PATCH 042/134] Apply configuration changes for Multiple LDAP servers --- config/initializers/1_settings.rb | 18 ++++++++++++++++-- config/initializers/7_omniauth.rb | 4 ++++ config/initializers/devise.rb | 30 ++++++++++++++++-------------- 3 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 config/initializers/7_omniauth.rb diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0d11ae6f33..abd0c97055 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -56,9 +56,23 @@ end # Default settings Settings['ldap'] ||= Settingslogic.new({}) Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? -Settings.ldap['allow_username_or_email_login'] = false if Settings.ldap['allow_username_or_email_login'].nil? -Settings.ldap['active_directory'] = true if Settings.ldap['active_directory'].nil? +# backwards compatibility, we only have one host +if Settings.ldap['enabled'] || Rails.env.test? + if Settings.ldap['host'].present? + server = Settings.ldap.except('sync_time') + server['label'] = 'LDAP' + server['provider_id'] = '' + Settings.ldap['servers'] = [server] + end + + Settings.ldap['servers'].each do |server| + server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? + server['active_directory'] = true if server['active_directory'].nil? + server['provider_name'] = "ldap#{server['provider_id']}".downcase + server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) + end +end Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb new file mode 100644 index 0000000000..1f569dbe91 --- /dev/null +++ b/config/initializers/7_omniauth.rb @@ -0,0 +1,4 @@ +module OmniAuth::Strategies + server = Gitlab.config.ldap.servers.first + const_set(server.provider_class, Class.new(LDAP)) +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 34f4f38698..7770f018a1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -205,21 +205,23 @@ Devise.setup do |config| # end if Gitlab.config.ldap.enabled - if Gitlab.config.ldap.allow_username_or_email_login - email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} - else - email_stripping_proc = ->(name) {name} - end + Gitlab.config.ldap.servers.each do |server| + if server['allow_username_or_email_login'] + email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} + else + email_stripping_proc = ->(name) {name} + end - config.omniauth :ldap, - host: Gitlab.config.ldap['host'], - base: Gitlab.config.ldap['base'], - uid: Gitlab.config.ldap['uid'], - port: Gitlab.config.ldap['port'], - method: Gitlab.config.ldap['method'], - bind_dn: Gitlab.config.ldap['bind_dn'], - password: Gitlab.config.ldap['password'], - name_proc: email_stripping_proc + config.omniauth server.provider_name, + host: server['host'], + base: server['base'], + uid: server['uid'], + port: server['port'], + method: server['method'], + bind_dn: server['bind_dn'], + password: server['password'], + name_proc: email_stripping_proc + end end Gitlab.config.omniauth.providers.each do |provider| From 4e0da2325b689221cb7f675648380fcbc2a9a492 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Oct 2014 15:15:34 +0300 Subject: [PATCH 043/134] Admin: user sorting --- app/controllers/admin/users_controller.rb | 1 + app/finders/snippets_finder.rb | 22 +++++++++--------- app/models/user.rb | 10 ++++++++ app/views/admin/users/index.html.haml | 20 ++++++++++++++++ spec/models/user_spec.rb | 28 +++++++++++++++++++++++ 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f63df27eeb..baad9095b7 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -4,6 +4,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.filter(params[:filter]) @users = @users.search(params[:name]) if params[:name].present? + @users = @users.sort(@sort = params[:sort]) @users = @users.alphabetically.page(params[:page]) end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index fda375aca2..b29ab6cf40 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -30,18 +30,18 @@ class SnippetsFinder snippets = user.snippets.fresh.non_expired if user == current_user - snippets = case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end + case scope + when 'are_internal' then + snippets.are_internal + when 'are_private' then + snippets.are_private + when 'are_public' then + snippets.are_public + else + snippets + end else - snippets = snippets.public_and_internal + snippets.public_and_internal end end diff --git a/app/models/user.rb b/app/models/user.rb index c90f246242..c6baa7ee70 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -196,6 +196,16 @@ class User < ActiveRecord::Base end end + def sort(method) + case method.to_s + when 'recent_sign_in' then reorder('users.last_sign_in_at DESC') + when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC') + when 'recently_created' then reorder('users.created_at DESC') + when 'late_created' then reorder('users.created_at ASC') + else reorder("users.name ASC") + end + end + def find_for_commit(email, name) # Prefer email match over name match User.where(email: email).first || diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 5c2664e14f..92c619738a 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,6 +32,26 @@ .panel-heading Users (#{@users.total_count}) .panel-head-actions + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu + %li + = link_to admin_users_path(sort: nil) do + Name + = link_to admin_users_path(sort: 'recent_sign_in') do + Recent sign in + = link_to admin_users_path(sort: 'oldest_sign_in') do + Oldest sign in + = link_to admin_users_path(sort: 'recently_created') do + Recently created + = link_to admin_users_path(sort: 'late_created') do + Late created = link_to 'New User', new_admin_user_path, class: "btn btn-new" %ul.well-list - @users.each do |user| diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0250014bc2..8c79bf5f3c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -429,4 +429,32 @@ describe User do expect(user.starred?(project)).to be_false end end + + describe "#sort" do + before do + User.delete_all + @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha' + @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega' + end + + it "sorts users as recently_signed_in" do + User.sort('recent_sign_in').first.should == @user + end + + it "sorts users as late_signed_in" do + User.sort('oldest_sign_in').first.should == @user1 + end + + it "sorts users as recently_created" do + User.sort('recently_created').first.should == @user + end + + it "sorts users as late_created" do + User.sort('late_created').first.should == @user1 + end + + it "sorts users by name when nil is passed" do + User.sort(nil).first.should == @user + end + end end From fc6a291af3fb849ea590481d7df359fe37798458 Mon Sep 17 00:00:00 2001 From: HerrBerg Date: Mon, 13 Oct 2014 16:26:35 +0200 Subject: [PATCH 044/134] fix exclude wiki regex the new regex allows importing repositories with repository name ending with wiki but still exclude gitlab wiki repositories --- lib/tasks/gitlab/import.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index b6ed874e11..159568f288 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -34,7 +34,7 @@ namespace :gitlab do puts "Processing #{repo_path}".yellow - if path =~ /.wiki\Z/ + if path =~ /\.wiki\Z/ puts " * Skipping wiki repo" next end From 01b791237cf6a1b7deaee3da3df6541e0b5107d1 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Mon, 13 Oct 2014 17:24:05 +0200 Subject: [PATCH 045/134] Refactor lib files for multiple LDAP groups --- lib/gitlab/auth.rb | 6 +- lib/gitlab/ldap/access.rb | 32 +++++--- lib/gitlab/ldap/adapter.rb | 63 ++++----------- lib/gitlab/ldap/authentication.rb | 68 ++++++++++++++++ lib/gitlab/ldap/config.rb | 115 ++++++++++++++++++++++++++++ lib/gitlab/ldap/person.rb | 34 ++++---- lib/gitlab/ldap/user.rb | 48 +++--------- spec/lib/gitlab/ldap/access_spec.rb | 26 +++---- 8 files changed, 262 insertions(+), 130 deletions(-) create mode 100644 lib/gitlab/ldap/authentication.rb create mode 100644 lib/gitlab/ldap/config.rb diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 955abc1bed..f97c0247b6 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -3,11 +3,13 @@ module Gitlab def find(login, password) user = User.find_by(email: login) || User.find_by(username: login) + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP if user.nil? || user.ldap_user? # Second chance - try LDAP authentication - return nil unless ldap_conf.enabled + return nil unless Gitlab::LDAP::Config.enabled? - Gitlab::LDAP::User.authenticate(login, password) + Gitlab::LDAP::Authentication.login(login, password) else user if user.valid_password?(password) end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index d2235d2e3b..111c750226 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -1,18 +1,21 @@ +# LDAP authorization model +# +# * Check if we are allowed access (not blocked) +# module Gitlab module LDAP class Access - attr_reader :adapter + attr_reader :adapter, :provider, :user - def self.open(&block) - Gitlab::LDAP::Adapter.open do |adapter| - block.call(self.new(adapter)) + def self.open(user, &block) + Gitlab::LDAP::Adapter.open(user.provider) do |adapter| + block.call(self.new(user, adapter)) end end def self.allowed?(user) - self.open do |access| - if access.allowed?(user) - # GitLab EE LDAP code goes here + self.open(user) do |access| + if access.allowed? user.last_credential_check_at = Time.now user.save true @@ -22,21 +25,26 @@ module Gitlab end end - def initialize(adapter=nil) + def initialize(user, adapter=nil) @adapter = adapter + @user = user + @provider = user.provider end - def allowed?(user) + def allowed? if Gitlab::LDAP::Person.find_by_dn(user.extern_uid, adapter) - if Gitlab.config.ldap.active_directory - !Gitlab::LDAP::Person.disabled_via_active_directory?(user.extern_uid, adapter) - end + return true unless ldap_config.active_directory + !Gitlab::LDAP::Person.disabled_via_active_directory?(user.extern_uid, adapter) else false end rescue false end + + def adapter + @adapter ||= Gitlab::LDAP::Adapter.new(provider) + end end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 68ac1b2290..c4d0a20d89 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -1,52 +1,25 @@ module Gitlab module LDAP class Adapter - attr_reader :ldap + attr_reader :provider, :ldap - def self.open(&block) - Net::LDAP.open(adapter_options) do |ldap| - block.call(self.new(ldap)) + def self.open(provider, &block) + Net::LDAP.open(config(provider).adapter_options) do |ldap| + block.call(self.new(provider, ldap)) end end - def self.config - Gitlab.config.ldap + def self.config(provider) + Gitlab::LDAP::Config.new(provider) end - def self.adapter_options - encryption = - case config['method'].to_s - when 'ssl' - :simple_tls - when 'tls' - :start_tls - else - nil - end - - options = { - host: config['host'], - port: config['port'], - encryption: encryption - } - - auth_options = { - auth: { - method: :simple, - username: config['bind_dn'], - password: config['password'] - } - } - - if config['password'] || config['bind_dn'] - options.merge!(auth_options) - end - options + def initialize(provider, ldap=nil) + @provider = provider + @ldap = ldap || Net::LDAP.new(config.adapter_options) end - - def initialize(ldap=nil) - @ldap = ldap || Net::LDAP.new(self.class.adapter_options) + def config + Gitlab::LDAP::Config.new(provider) end def users(field, value) @@ -57,13 +30,13 @@ module Gitlab } else options = { - base: config['base'], + base: config.base, filter: Net::LDAP::Filter.eq(field, value) } end - if config['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(config['user_filter']) + if config.user_filter.present? + user_filter = Net::LDAP::Filter.construct(config.user_filter) options[:filter] = if options[:filter] Net::LDAP::Filter.join(options[:filter], user_filter) @@ -77,7 +50,7 @@ module Gitlab end entries.map do |entry| - Gitlab::LDAP::Person.new(entry) + Gitlab::LDAP::Person.new(entry, provider) end end @@ -105,12 +78,6 @@ module Gitlab results end end - - private - - def config - @config ||= self.class.config - end end end end diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb new file mode 100644 index 0000000000..0eca9b2613 --- /dev/null +++ b/lib/gitlab/ldap/authentication.rb @@ -0,0 +1,68 @@ +# This calls helps to authenticate to LDAP by providing username and password +# +# Since multiple LDAP servers are supported, it will loop through all of them +# until a valid bind is found +# + +module Gitlab + module LDAP + class Authentication + def self.login(login, password) + return unless Gitlab::LDAP::Config.enabled? + return unless login.present? && password.present? + + auth = nil + # loop through providers until valid bind + providers.find do |provider| + auth = new(provider) + auth.login(login, password) # true will exit the loop + end + + auth.user + end + + def self.providers + Gitlab::LDAP::Config.providers + end + + attr_accessor :provider, :ldap_user + + def initialize(provider) + @provider = provider + end + + def login(login, password) + @ldap_user = adapter.bind_as( + filter: user_filter(login), + size: 1, + password: password + ) + end + + def adapter + OmniAuth::LDAP::Adaptor.new(config.options) + end + + def config + Gitlab::LDAP::Config.new(provider) + end + + def user_filter(login) + Net::LDAP::Filter.eq(config.uid, login).tap do |filter| + # Apply LDAP user filter if present + if config.user_filter.present? + Net::LDAP::Filter.join( + filter, + Net::LDAP::Filter.construct(config.user_filter) + ) + end + end + end + + def user + return nil unless ldap_user + Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider) + end + end + end +end \ No newline at end of file diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb new file mode 100644 index 0000000000..697b66dcda --- /dev/null +++ b/lib/gitlab/ldap/config.rb @@ -0,0 +1,115 @@ +# Load a specific server configuration +module Gitlab + module LDAP + class Config + attr_accessor :provider, :options + + def self.enabled? + Gitlab.config.ldap.enabled + end + + def self.servers + Gitlab.config.ldap.servers + end + + def self.providers + servers.map &:provider_name + end + + def initialize(provider) + @provider = provider + invalid_provider unless valid_provider? + @options = config_for(provider) + end + + def enabled? + base_config.enabled + end + + def adapter_options + { + host: options['host'], + port: options['port'], + encryption: encryption + }.tap do |options| + options.merge!(auth_options) if has_auth? + end + end + + def base + options['base'] + end + + def uid + options['uid'] + end + + def sync_ssh_keys? + sync_ssh_keys.present? + end + + # The LDAP attribute in which the ssh keys are stored + def sync_ssh_keys + options['sync_ssh_keys'] + end + + def user_filter + options['user_filter'] + end + + def group_base + options['group_base'] + end + + def admin_group + options['admin_group'] + end + + def active_directory + options['active_directory'] + end + + protected + def base_config + Gitlab.config.ldap + end + + def config_for(provider) + base_config.servers.find { |server| server.provider_name == provider } + end + + def encryption + case options['method'].to_s + when 'ssl' + :simple_tls + when 'tls' + :start_tls + else + nil + end + end + + def valid_provider? + self.class.providers.include?(provider) + end + + def invalid_provider + raise "Unknown provider (#{provider}). Available providers: #{self.class.providers}" + end + + def auth_options + { + auth: { + method: :simple, + username: options['bind_dn'], + password: options['password'] + } + } + end + + def has_auth? + options['password'] || options['bind_dn'] + end + end + end +end diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 87c3d711db..a35fd22073 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -6,24 +6,24 @@ module Gitlab # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2") - def self.find_by_uid(uid, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new - adapter.user(config.uid, uid) + attr_accessor :entry, :provider + + def self.find_by_uid(uid, adapter) + adapter.user(Gitlab.config.ldap.uid, uid) end - def self.find_by_dn(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.find_by_dn(dn, adapter) adapter.user('dn', dn) end - def self.disabled_via_active_directory?(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.disabled_via_active_directory?(dn, adapter) adapter.dn_matches_filter?(dn, AD_USER_DISABLED) end - def initialize(entry) + def initialize(entry, provider) Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry + @provider = provider end def name @@ -38,22 +38,30 @@ module Gitlab uid end + def email + entry.try(:mail) + end + def dn entry.dn end + def ssh_keys + if config.sync_ssh_keys? && entry.respond_to?(config.sync_ssh_keys) + entry[config.sync_ssh_keys.to_sym] + else + [] + end + end + private def entry @entry end - def adapter - @adapter ||= Gitlab::LDAP::Adapter.new - end - def config - @config ||= Gitlab.config.ldap + @config ||= Gitlab::LDAP::Config.new(provider) end end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 006ef17072..3069027a42 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -10,45 +10,11 @@ module Gitlab module LDAP class User < Gitlab::OAuth::User class << self - def authenticate(login, password) - # Check user against LDAP backend if user is not authenticated - # Only check with valid login and password to prevent anonymous bind results - return nil unless ldap_conf.enabled && login.present? && password.present? - - ldap_user = adapter.bind_as( - filter: user_filter(login), - size: 1, - password: password - ) - - find_by_uid(ldap_user.dn) if ldap_user - end - - def adapter - @adapter ||= OmniAuth::LDAP::Adaptor.new(ldap_conf) - end - - def user_filter(login) - filter = Net::LDAP::Filter.eq(adapter.uid, login) - # Apply LDAP user filter if present - if ldap_conf['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter']) - filter = Net::LDAP::Filter.join(filter, user_filter) - end - filter - end - - def ldap_conf - Gitlab.config.ldap - end - - def find_by_uid(uid) + def find_by_uid_and_provider(uid, provider) # LDAP distinguished name is case-insensitive - model.where("provider = ? and lower(extern_uid) = ?", provider, uid.downcase).last - end - - def provider - 'ldap' + ::User. + where(provider: [provider, :ldap]). + where('lower(extern_uid) = ?', uid.downcase).last end end @@ -65,7 +31,7 @@ module Gitlab def find_by_uid_and_provider # LDAP distinguished name is case-insensitive model. - where(provider: auth_hash.provider). + where(provider: [auth_hash.provider, :ldap]). where('lower(extern_uid) = ?', auth_hash.uid.downcase).last end @@ -88,6 +54,10 @@ module Gitlab def needs_blocking? false end + + def allowed? + Gitlab::LDAP::Access.allowed?(gl_user) + end end end end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index d50f605e05..f4d5a92739 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe Gitlab::LDAP::Access do - let(:access) { Gitlab::LDAP::Access.new } - let(:user) { create(:user) } + let(:access) { Gitlab::LDAP::Access.new user } + let(:user) { create(:user, :ldap) } describe :allowed? do - subject { access.allowed?(user) } + subject { access.allowed? } context 'when the user cannot be found' do before { Gitlab::LDAP::Person.stub(find_by_dn: nil) } @@ -28,20 +28,14 @@ describe Gitlab::LDAP::Access do it { should be_true } end - context 'and has no disabled flag in active diretory' do - before { - Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) - Gitlab.config.ldap['enabled'] = true - Gitlab.config.ldap['active_directory'] = false - } + context 'without ActiveDirectory enabled' do + before do + Gitlab::LDAP::Config.stub(enabled?: true) + Gitlab::LDAP::Config.any_instance.stub(active_directory: false) + end - after { - Gitlab.config.ldap['enabled'] = false - Gitlab.config.ldap['active_directory'] = true - } - - it { should be_false } + it { should be_true } end end end -end +end \ No newline at end of file From 5e1c39cb783843163edf47718fa6d39b4ebb52e1 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Mon, 13 Oct 2014 17:33:44 +0200 Subject: [PATCH 046/134] Merge tests to support Multiple LDAP groups --- spec/factories.rb | 5 +++++ spec/lib/gitlab/auth_spec.rb | 7 +++---- spec/lib/gitlab/ldap/adapter_spec.rb | 2 +- spec/lib/gitlab/ldap/user_spec.rb | 22 +++------------------- spec/models/user_spec.rb | 19 +++++++++++++++++++ 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index a960571206..15899d8c3c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -24,6 +24,11 @@ FactoryGirl.define do admin true end + trait :ldap do + provider 'ldapmain' + extern_uid 'my-ldap-id' + end + factory :admin, traits: [:admin] end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 551fb3fb5f..1f3e1a4a3c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -28,17 +28,16 @@ describe Gitlab::Auth do end context "with ldap enabled" do - before { Gitlab.config.ldap['enabled'] = true } - after { Gitlab.config.ldap['enabled'] = false } + before { Gitlab::LDAP::Config.stub(enabled?: true) } it "tries to autheticate with db before ldap" do - expect(Gitlab::LDAP::User).not_to receive(:authenticate) + expect(Gitlab::LDAP::Authentication).not_to receive(:login) gl_auth.find(username, password) end it "uses ldap as fallback to for authentication" do - expect(Gitlab::LDAP::User).to receive(:authenticate) + expect(Gitlab::LDAP::Authentication).to receive(:login) gl_auth.find('ldap_user', 'password') end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index c3f0733443..19347e4737 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::LDAP::Adapter do - let(:adapter) { Gitlab::LDAP::Adapter.new } + let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' } describe :dn_matches_filter? do let(:ldap) { double(:ldap) } diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index a1aec0bb96..726c9764e3 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -10,12 +10,12 @@ describe Gitlab::LDAP::User do } end let(:auth_hash) do - double(uid: 'my-uid', provider: 'ldap', info: double(info)) + double(uid: 'my-uid', provider: 'ldapmain', info: double(info)) end describe :find_or_create do it "finds the user if already existing" do - existing_user = create(:user, extern_uid: 'my-uid', provider: 'ldap') + existing_user = create(:user, extern_uid: 'my-uid', provider: 'ldapmain') expect{ gl_user.save }.to_not change{ User.count } end @@ -26,27 +26,11 @@ describe Gitlab::LDAP::User do existing_user.reload expect(existing_user.extern_uid).to eql 'my-uid' - expect(existing_user.provider).to eql 'ldap' + expect(existing_user.provider).to eql 'ldapmain' end it "creates a new user if not found" do expect{ gl_user.save }.to change{ User.count }.by(1) end end - - describe "authenticate" do - let(:login) { 'john' } - let(:password) { 'my-secret' } - - before { - Gitlab.config.ldap['enabled'] = true - Gitlab.config.ldap['user_filter'] = 'employeeType=developer' - } - after { Gitlab.config.ldap['enabled'] = false } - - it "send an authentication request to ldap" do - expect( Gitlab::LDAP::User.adapter ).to receive(:bind_as) - Gitlab::LDAP::User.authenticate(login, password) - end - end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8c79bf5f3c..6ad57b06e0 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -346,6 +346,25 @@ describe User do end end + describe :ldap_user? do + let(:user) { build(:user, :ldap) } + + it "is true if provider name starts with ldap" do + user.provider = 'ldapmain' + expect( user.ldap_user? ).to be_true + end + + it "is false for other providers" do + user.provider = 'other-provider' + expect( user.ldap_user? ).to be_false + end + + it "is false if no extern_uid is provided" do + user.extern_uid = nil + expect( user.ldap_user? ).to be_false + end + end + describe '#full_website_url' do let(:user) { create(:user) } From 014919340a27c771b280452b30913bc6b0a791e5 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Mon, 13 Oct 2014 09:07:23 -0700 Subject: [PATCH 047/134] update changelog Change log entry for https://github.com/gitlabhq/gitlabhq/pull/8020 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index c98d21f986..316d7af174 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 7.4.0 - Show build coverage in Merge Requests (requires GitLab CI v5.1) - New milestone and label links on issue edit form - Improved repository graphs + - Improve event note display in dashboard and project activity views (Vinnie Okada) v 7.3.2 - Fix creating new file via web editor From 2d235221079ef6af90bf482a8f563dd409290751 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Thu, 25 Sep 2014 16:43:23 +0200 Subject: [PATCH 048/134] Use :message key, not :error for File::Service. --- app/controllers/projects/blob_controller.rb | 2 +- .../projects/edit_tree_controller.rb | 2 +- app/services/files/base_service.rb | 6 ------ features/project/source/browse_files.feature | 20 +++++++++++++++++++ features/steps/project/source/browse_files.rb | 8 ++++++++ features/steps/shared/paths.rb | 9 +++++++++ lib/api/files.rb | 6 +++--- 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7009e3b1bc..0944c7421e 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -20,7 +20,7 @@ class Projects::BlobController < Projects::ApplicationController flash[:notice] = "Your changes have been successfully committed" redirect_to project_tree_path(@project, @ref) else - flash[:alert] = result[:error] + flash[:alert] = result[:message] render :show end end diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb index 8976d7c7be..fdc1a85d8d 100644 --- a/app/controllers/projects/edit_tree_controller.rb +++ b/app/controllers/projects/edit_tree_controller.rb @@ -22,7 +22,7 @@ class Projects::EditTreeController < Projects::BaseTreeController redirect_to after_edit_path else - flash[:alert] = result[:error] + flash[:alert] = result[:message] render :show end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index db6f0831f8..bd24510095 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -10,12 +10,6 @@ module Files private - def success - out = super() - out[:error] = '' - out - end - def repository project.repository end diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index aca255b944..b7d70881d5 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -34,6 +34,16 @@ Feature: Project Source Browse Files Then I am redirected to the new file And I should see its new content + @javascript + Scenario: If I enter an illegal file name I see an error message + Given I click on "new file" link in repo + And I fill the new file name with an illegal name + And I edit code + And I fill the commit message + And I click on "Commit changes" + Then I am on the new file page + And I see a commit error message + @javascript Scenario: I can edit file Given I click on ".gitignore" file in repo @@ -50,6 +60,16 @@ Feature: Project Source Browse Files Then I am redirected to the ".gitignore" And I should see its new content + @javascript @wip + Scenario: If I don't change the content of the file I see an error message + Given I click on ".gitignore" file in repo + And I click button "edit" + And I fill the commit message + And I click on "Commit changes" + # Test fails because carriage returns are added to the file. + Then I am on the ".gitignore" edit file page + And I see a commit error message + @javascript Scenario: I can see editing preview Given I click on ".gitignore" file in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 20f8f6c24a..665f5d6d19 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -61,6 +61,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps fill_in :file_name, with: new_file_name end + step 'I fill the new file name with an illegal name' do + fill_in :file_name, with: '.git' + end + step 'I fill the commit message' do fill_in :commit_message, with: 'Not yet a commit message.' end @@ -151,6 +155,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).not_to have_link('permalink') end + step 'I see a commit error message' do + expect(page).to have_content('Your changes could not be committed') + end + private def set_new_content diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 1f238f8bef..5f292255ce 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -265,6 +265,15 @@ module SharedPaths visit project_blob_path(@project, File.join(root_ref, '.gitignore')) end + step 'I am on the new file page' do + current_path.should eq(project_new_tree_path(@project, root_ref)) + end + + step 'I am on the ".gitignore" edit file page' do + current_path.should eq(project_edit_tree_path( + @project, File.join(root_ref, '.gitignore'))) + end + step 'I visit project source page for "6d39438"' do visit project_tree_path(@project, "6d39438") end diff --git a/lib/api/files.rb b/lib/api/files.rb index e63e635a4d..84e1d31178 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -85,7 +85,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end @@ -117,7 +117,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end @@ -149,7 +149,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end end From c278520f9b96347868ce4f65b0d59aa6197e333d Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 13 Oct 2014 21:21:58 +0200 Subject: [PATCH 049/134] Remove unused dev_tools helper. --- app/controllers/application_controller.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13d8d2a3e0..1c7fcac6a5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,7 +7,6 @@ class ApplicationController < ActionController::Base before_filter :check_password_expiration before_filter :add_abilities before_filter :ldap_security_check - before_filter :dev_tools if Rails.env == 'development' before_filter :default_headers before_filter :add_gon_variables before_filter :configure_permitted_parameters, if: :devise_controller? @@ -170,9 +169,6 @@ class ApplicationController < ActionController::Base response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end - def dev_tools - end - def default_headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' From a22d4cebb0c7687f7f8d97849e84125a0b3a52eb Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 13 Oct 2014 21:24:42 +0200 Subject: [PATCH 050/134] Remove unused filter from ProjectsController Neither controller nor any of it's descendants have those actions. --- app/controllers/projects_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b3380a6ff2..081df35b6c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -6,7 +6,6 @@ class ProjectsController < ApplicationController # Authorize before_filter :authorize_read_project!, except: [:index, :new, :create] before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive, :retry_import] - before_filter :require_non_empty_project, only: [:blob, :tree, :graph] layout 'navless', only: [:new, :create, :fork] before_filter :set_title, only: [:new, :create] From 4d0d5e79ba4317cedfb2b0304ac5d376ad781b1a Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 13 Oct 2014 21:31:49 +0200 Subject: [PATCH 051/134] Factor authorize_push! and authorize_code_access! with existing method_missing. Pattern already used extensively, so let's be consistent and use it everywhere. --- app/controllers/application_controller.rb | 8 -------- app/controllers/projects/base_tree_controller.rb | 2 +- app/controllers/projects/blame_controller.rb | 2 +- app/controllers/projects/blob_controller.rb | 4 ++-- app/controllers/projects/branches_controller.rb | 4 ++-- app/controllers/projects/commit_controller.rb | 2 +- app/controllers/projects/commits_controller.rb | 2 +- app/controllers/projects/compare_controller.rb | 2 +- app/controllers/projects/edit_tree_controller.rb | 2 +- app/controllers/projects/graphs_controller.rb | 2 +- app/controllers/projects/network_controller.rb | 2 +- app/controllers/projects/new_tree_controller.rb | 2 +- app/controllers/projects/raw_controller.rb | 2 +- app/controllers/projects/refs_controller.rb | 2 +- app/controllers/projects/repositories_controller.rb | 2 +- app/controllers/projects/tags_controller.rb | 4 ++-- 16 files changed, 18 insertions(+), 26 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13d8d2a3e0..e05cf623a6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -119,14 +119,6 @@ class ApplicationController < ActionController::Base return access_denied! unless can?(current_user, action, project) end - def authorize_code_access! - return access_denied! unless can?(current_user, :download_code, project) - end - - def authorize_push! - return access_denied! unless can?(current_user, :push_code, project) - end - def authorize_labels! # Labels should be accessible for issues and/or merge requests authorize_read_issue! || authorize_read_merge_request! diff --git a/app/controllers/projects/base_tree_controller.rb b/app/controllers/projects/base_tree_controller.rb index 5e30593443..56c306063c 100644 --- a/app/controllers/projects/base_tree_controller.rb +++ b/app/controllers/projects/base_tree_controller.rb @@ -2,7 +2,7 @@ class Projects::BaseTreeController < Projects::ApplicationController include ExtractsPath before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index a3c4130167..bad06e7aa2 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -4,7 +4,7 @@ class Projects::BlameController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7009e3b1bc..9234bc8cc1 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -4,9 +4,9 @@ class Projects::BlobController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project - before_filter :authorize_push!, only: [:destroy] + before_filter :authorize_push_code!, only: [:destroy] before_filter :blob diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index faa0ce67ca..dd6df5d196 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -3,8 +3,8 @@ class Projects::BranchesController < Projects::ApplicationController before_filter :authorize_read_project! before_filter :require_non_empty_project - before_filter :authorize_code_access! - before_filter :authorize_push!, only: [:create, :destroy] + before_filter :authorize_download_code! + before_filter :authorize_push_code!, only: [:create, :destroy] def index @sort = params[:sort] || 'name' diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 66c67b661d..8d053f1f03 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -4,7 +4,7 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project before_filter :commit diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index b7f09eb271..53a0d063d8 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -5,7 +5,7 @@ class Projects::CommitsController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7a671e8455..6d94402559 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,7 +1,7 @@ class Projects::CompareController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def index diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb index 8976d7c7be..2501561fa3 100644 --- a/app/controllers/projects/edit_tree_controller.rb +++ b/app/controllers/projects/edit_tree_controller.rb @@ -1,7 +1,7 @@ class Projects::EditTreeController < Projects::BaseTreeController before_filter :require_branch_head before_filter :blob - before_filter :authorize_push! + before_filter :authorize_push_code! before_filter :from_merge_request before_filter :after_edit_path diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 610b4967fe..21d3970d65 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,7 +1,7 @@ class Projects::GraphsController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 9832495c64..009089ee63 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -4,7 +4,7 @@ class Projects::NetworkController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb index 71a5c6499e..ffba706b2f 100644 --- a/app/controllers/projects/new_tree_controller.rb +++ b/app/controllers/projects/new_tree_controller.rb @@ -1,6 +1,6 @@ class Projects::NewTreeController < Projects::BaseTreeController before_filter :require_branch_head - before_filter :authorize_push! + before_filter :authorize_push_code! def show end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 5ec9c576a6..f4fdd616c5 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -4,7 +4,7 @@ class Projects::RawController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def show diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 7997c726fb..9ac189a78b 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -3,7 +3,7 @@ class Projects::RefsController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def switch diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 4e0f190ed1..6d8ef0f1ac 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,7 +1,7 @@ class Projects::RepositoriesController < Projects::ApplicationController # Authorize before_filter :authorize_read_project! - before_filter :authorize_code_access! + before_filter :authorize_download_code! before_filter :require_non_empty_project def archive diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 537c94bda2..94794fb5dd 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -3,8 +3,8 @@ class Projects::TagsController < Projects::ApplicationController before_filter :authorize_read_project! before_filter :require_non_empty_project - before_filter :authorize_code_access! - before_filter :authorize_push!, only: [:create] + before_filter :authorize_download_code! + before_filter :authorize_push_code!, only: [:create] before_filter :authorize_admin_project!, only: [:destroy] def index From 410d6e306b04a4bfd321996e1e6548032f8f8b85 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 08:54:15 +0200 Subject: [PATCH 052/134] Remove unused method --- lib/gitlab/ldap/person.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index a35fd22073..eae0a87a50 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -46,14 +46,6 @@ module Gitlab entry.dn end - def ssh_keys - if config.sync_ssh_keys? && entry.respond_to?(config.sync_ssh_keys) - entry[config.sync_ssh_keys.to_sym] - else - [] - end - end - private def entry From 93505f7d04cfbbc9565dc5759dbeb768515520e7 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 09:05:29 +0200 Subject: [PATCH 053/134] DRY find method to find Gitlab user --- lib/gitlab/ldap/user.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 3069027a42..9235f6310d 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -29,10 +29,8 @@ module Gitlab end def find_by_uid_and_provider - # LDAP distinguished name is case-insensitive - model. - where(provider: [auth_hash.provider, :ldap]). - where('lower(extern_uid) = ?', auth_hash.uid.downcase).last + self.class.find_by_uid_and_provider( + auth_hash.provider, auth_hash.uid.downcase) end def find_by_email From fc5bfd1dc1c963d018d4de61b03c5ba28aafcd18 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 09:22:59 +0200 Subject: [PATCH 054/134] Move dynamic omniauth declarations to initializer --- app/controllers/omniauth_callbacks_controller.rb | 4 ---- config/initializers/7_omniauth.rb | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 0f364a48ea..f46b36568f 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -32,10 +32,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end - Gitlab.config.ldap.servers.each do |server| - alias_method server.provider_name, :ldap - end - def omniauth_error @provider = params[:provider] @error = params[:error] diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 1f569dbe91..22e2d740fd 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -2,3 +2,8 @@ module OmniAuth::Strategies server = Gitlab.config.ldap.servers.first const_set(server.provider_class, Class.new(LDAP)) end + +OmniauthCallbacksController.class_eval do + server = Gitlab.config.ldap.servers.first + alias_method server.provider_name, :ldap +end \ No newline at end of file From 23b692c701f0d310d6069fc74656f2bfdfdfc360 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 09:31:38 +0200 Subject: [PATCH 055/134] Add specs for authentication and config --- spec/lib/gitlab/ldap/authentication_spec.rb | 53 +++++++++++++++++++++ spec/lib/gitlab/ldap/config_spec.rb | 20 ++++++++ 2 files changed, 73 insertions(+) create mode 100644 spec/lib/gitlab/ldap/authentication_spec.rb create mode 100644 spec/lib/gitlab/ldap/config_spec.rb diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb new file mode 100644 index 0000000000..0eb7c443b8 --- /dev/null +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Authentication do + let(:klass) { Gitlab::LDAP::Authentication } + let(:user) { create(:user, :ldap, extern_uid: dn) } + let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } + let(:login) { 'john' } + let(:password) { 'password' } + + describe :login do + let(:adapter) { double :adapter } + before do + Gitlab::LDAP::Config.stub(enabled?: true) + end + + it "finds the user if authentication is successful" do + user + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, + bind_as: double(:ldap_user, dn: dn) + )) + expect(klass.login(login, password)).to be_true + end + + it "is false if the user does not exist" do + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, + bind_as: double(:ldap_user, dn: dn) + )) + expect(klass.login(login, password)).to be_false + end + + it "is false if authentication fails" do + user + # try only to fake the LDAP call + klass.any_instance.stub(adapter: double(:adapter, bind_as: nil)) + expect(klass.login(login, password)).to be_false + end + + it "fails if ldap is disabled" do + Gitlab::LDAP::Config.stub(enabled?: false) + expect(klass.login(login, password)).to be_false + end + + it "fails if no login is supplied" do + expect(klass.login('', password)).to be_false + end + + it "fails if no password is supplied" do + expect(klass.login(login, '')).to be_false + end + end +end \ No newline at end of file diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb new file mode 100644 index 0000000000..76cc7f95c4 --- /dev/null +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Config do + let(:config) { Gitlab::LDAP::Config.new provider } + let(:provider) { 'ldapmain' } + + describe :initalize do + it 'requires a provider' do + expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError + end + + it "works" do + expect(config).to be_a described_class + end + + it "raises an error if a unknow provider is used" do + expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error + end + end +end \ No newline at end of file From b229b0f00327b210374d847b57760757fdcd8ee3 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 09:40:35 +0200 Subject: [PATCH 056/134] Fix authorization for LDAP login --- lib/gitlab/ldap/access.rb | 4 ++++ lib/gitlab/ldap/user.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 111c750226..eb2c4e48ff 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -45,6 +45,10 @@ module Gitlab def adapter @adapter ||= Gitlab::LDAP::Adapter.new(provider) end + + def ldap_config + Gitlab::LDAP::Config.new(provider) + end end end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 9235f6310d..3176e9790a 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -30,7 +30,7 @@ module Gitlab def find_by_uid_and_provider self.class.find_by_uid_and_provider( - auth_hash.provider, auth_hash.uid.downcase) + auth_hash.uid.downcase, auth_hash.provider) end def find_by_email From d3056feb119de28a0e7333f80ee6d42ecf690dc5 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 10:08:47 +0200 Subject: [PATCH 057/134] Make sure the filters are applied --- lib/gitlab/ldap/authentication.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index 0eca9b2613..8d306a74c1 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -48,15 +48,16 @@ module Gitlab end def user_filter(login) - Net::LDAP::Filter.eq(config.uid, login).tap do |filter| - # Apply LDAP user filter if present - if config.user_filter.present? - Net::LDAP::Filter.join( - filter, - Net::LDAP::Filter.construct(config.user_filter) - ) - end + filter = Net::LDAP::Filter.eq(config.uid, login) + + # Apply LDAP user filter if present + if config.user_filter.present? + filter = Net::LDAP::Filter.join( + filter, + Net::LDAP::Filter.construct(config.user_filter) + ) end + filter end def user From 18d2ee31e81e617a4c860359cb29f9118b8a3e70 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 10:54:43 +0200 Subject: [PATCH 058/134] Use server specific uid --- lib/gitlab/ldap/person.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index eae0a87a50..3e0b3e6cbf 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -9,7 +9,7 @@ module Gitlab attr_accessor :entry, :provider def self.find_by_uid(uid, adapter) - adapter.user(Gitlab.config.ldap.uid, uid) + adapter.user(adapter.config.uid, uid) end def self.find_by_dn(dn, adapter) From ab04096c6cf5ff340b17df56afeae9782464742d Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 11:14:57 +0200 Subject: [PATCH 059/134] Add explaining note to authentication method [skip ci] --- lib/gitlab/ldap/authentication.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index 8d306a74c1..a5944f9698 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -18,6 +18,8 @@ module Gitlab auth.login(login, password) # true will exit the loop end + # If (login, password) was invalid for all providers, the value of auth is now the last + # Gitlab::LDAP::Authentication instance we tried. auth.user end From 9abe5d36d79971a8dda3626a2bab90e42c401014 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 11:23:33 +0200 Subject: [PATCH 060/134] Add libravatar documentation. --- doc/README.md | 1 + doc/customization/libravatar.md | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 doc/customization/libravatar.md diff --git a/doc/README.md b/doc/README.md index 2f90cf14a6..a8e21f7571 100644 --- a/doc/README.md +++ b/doc/README.md @@ -20,6 +20,7 @@ - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. ## Contributor documentation diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md new file mode 100644 index 0000000000..4dffd3027a --- /dev/null +++ b/doc/customization/libravatar.md @@ -0,0 +1,69 @@ +# Use Libravatar service with GitLab + +GitLab by default supports [Gravatar](gravatar.com) avatar service. +Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is +[heavily based on gravatar](http://wiki.libravatar.org/api/). + +This means that it is not complicated to switch to Libravatar avatar service or even self hosted Libravatar server. + +# Configuration + +In [gitlab.yml gravatar section](https://gitlab.com/gitlab-org/gitlab-ce/blob/672bd3902d86b78d730cea809fce312ec49d39d7/config/gitlab.yml.example#L122) set +the configuration options as follows: + +## For HTTP + +```yml + gravatar: + enabled: true + # gravatar urls: possible placeholders: %{hash} %{size} %{email} + plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +## For HTTPS + +```yml + gravatar: + enabled: true + # gravatar urls: possible placeholders: %{hash} %{size} %{email} + ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +## Self-hosted + +If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the url will be different in the configuration +but the important part is to provide the same placeholders so GitLab can parse the url correctly. + +For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is + +`http://libravatar.example.com/avatar/%{hash}?s=%{size}&d=identicon` + + +## Omnibus-gitlab example + +In `/etc/gitlab/gitlab.rb`: + +#### For http + +```ruby +gitlab_rails['gravatar_enabled'] = true +gitlab_rails['gravatar_plain_url'] = "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + +#### For https + +```ruby +gitlab_rails['gravatar_enabled'] = true +gitlab_rails['gravatar_ssl_url'] = "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" +``` + + +Run `sudo gitlab-ctl reconfigure` for changes to take effect. + + +## Default URL for missing images + +[Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. + +In order to use a different set other than `identicon`, replace `&d=identicon` portion of the url with another supported set. +For example, you can use `retro` set in which case url would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` From 920eb7abc4b3e7006ca6cffd4de2b290ddfe3d6d Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 12:02:05 +0200 Subject: [PATCH 061/134] Add a note about notification for a project. --- app/views/profiles/notifications/show.html.haml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index f84de4430c..ce84b4c43e 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -31,12 +31,12 @@ .clearfix %hr - %p - You can also specify notification level per group or per project - %br - By default all projects and groups uses notification level set above .row.all-notifications .col-md-6 + %p + You can also specify notification level per group or per project + %br + By default all projects and groups uses notification level set above %h4 Groups: %ul.bordered-list - @group_members.each do |users_group| @@ -44,6 +44,10 @@ = render 'settings', type: 'group', membership: users_group, notification: notification .col-md-6 + %p + To specify notification level per project of a group you belong to, + %br + you also need to be a member of the project %h4 Projects: %ul.bordered-list - @project_members.each do |project_member| From 9bf7bfda20a466b375a459b95068de8c0139fc9a Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 12:09:24 +0200 Subject: [PATCH 062/134] Remove unused methods --- lib/gitlab/auth.rb | 8 -------- lib/gitlab/oauth/user.rb | 4 ---- 2 files changed, 12 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index f97c0247b6..ae33c529b9 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -14,13 +14,5 @@ module Gitlab user if user.valid_password?(password) end end - - def log - Gitlab::AppLogger - end - - def ldap_conf - @ldap_conf ||= Gitlab.config.ldap - end end end diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 699258baee..133445d3d0 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -70,10 +70,6 @@ module Gitlab Gitlab::AppLogger end - def raise_error(message) - raise OmniAuth::Error, "(OAuth) " + message - end - def needs_blocking? Gitlab.config.omniauth['block_auto_created_users'] end From f174800b207d8bcbab46bdbeb65bf219eecf6d83 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 12:28:30 +0200 Subject: [PATCH 063/134] Different wording for notification note. --- app/views/profiles/notifications/show.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index ce84b4c43e..a044fad8fa 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -34,9 +34,9 @@ .row.all-notifications .col-md-6 %p - You can also specify notification level per group or per project + You can also specify notification level per group or per project. %br - By default all projects and groups uses notification level set above + By default all projects and groups uses notification level set above. %h4 Groups: %ul.bordered-list - @group_members.each do |users_group| @@ -47,7 +47,7 @@ %p To specify notification level per project of a group you belong to, %br - you also need to be a member of the project + you need to be a member of the project itself, not only its group. %h4 Projects: %ul.bordered-list - @project_members.each do |project_member| From 1d432e96bc44ad2f7f67dc4a551d34083f51f281 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 12:31:47 +0200 Subject: [PATCH 064/134] Add a link to libravatar doc in gitlab.yml.example. --- config/gitlab.yml.example | 1 + 1 file changed, 1 insertion(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 857643c006..7f624f92a8 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -119,6 +119,7 @@ production: &base # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" ## Gravatar + ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: enabled: true # Use user avatar image from Gravatar.com (default: true) # gravatar urls: possible placeholders: %{hash} %{size} %{email} From 6ce65a3e950532e8fb65cf188eb5df9a6eddfb39 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 13:11:53 +0200 Subject: [PATCH 065/134] Use Hash syntax for LDAP server declaration --- app/controllers/sessions_controller.rb | 2 +- app/views/devise/sessions/new.html.haml | 6 +++--- config/gitlab.yml.example | 22 ++++++++++------------ config/initializers/1_settings.rb | 10 ++++++---- config/initializers/7_omniauth.rb | 8 ++++---- config/initializers/devise.rb | 4 ++-- lib/gitlab/ldap/config.rb | 6 +++--- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index e918f46bb3..5ced98152a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -19,7 +19,7 @@ class SessionsController < Devise::SessionsController end if Gitlab.config.ldap.enabled - @ldap_servers = Gitlab.config.ldap.servers + @ldap_servers = Gitlab::LDAP::Config.servers end super diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 04e998f8be..b983278744 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -6,13 +6,13 @@ %ul.nav.nav-tabs - @ldap_servers.each_with_index do |server, i| %li{class: (:active if i==0)} - = link_to server['label'], "#tab-#{server.provider_name}", 'data-toggle' => 'tab' + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' %li = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' .tab-content - @ldap_servers.each_with_index do |server,i| - %div.tab-pane{id: "tab-#{server.provider_name}", class: (:active if i==0)} - = render 'devise/sessions/new_ldap', provider: server.provider_name + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i==0)} + = render 'devise/sessions/new_ldap', provider: server['provider_name'] %div#tab-signin.tab-pane = render 'devise/sessions/new_base' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 9302dca4ed..59bd144299 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -135,18 +135,16 @@ production: &base ldap: enabled: false servers: - - - ## provider_id - # - # This identifier is used by GitLab to keep track of which LDAP server each - # GitLab user belongs to. Each LDAP server known to GitLab should have a unique - # provider_id. This identifier cannot be changed once users from the LDAP server - # have started logging in to GitLab. - # - # Format: one word, using a-z (lower case) and 0-9 - # Example: 'paris' or 'uswest2' - - provider_id: main + ## provider id + # + # This identifier is used by GitLab to keep track of which LDAP server each + # GitLab user belongs to. Each LDAP server known to GitLab should have a unique + # provider id. This identifier cannot be changed once users from the LDAP server + # have started logging in to GitLab. + # + # Format: one word, using a-z (lower case) and 0-9 + # Example: 'paris' or 'uswest2' + main: ## label # diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index abd0c97055..7e7c91ced7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -62,14 +62,16 @@ if Settings.ldap['enabled'] || Rails.env.test? if Settings.ldap['host'].present? server = Settings.ldap.except('sync_time') server['label'] = 'LDAP' - server['provider_id'] = '' - Settings.ldap['servers'] = [server] + server['provider_name'] = 'ldap' + Settings.ldap['servers'] = { + 'ldap' => server + } end - Settings.ldap['servers'].each do |server| + Settings.ldap['servers'].each do |key, server| server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? - server['provider_name'] = "ldap#{server['provider_id']}".downcase + server['provider_name'] ||= "ldap#{key}".downcase server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) end end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 22e2d740fd..7ef5c10da0 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -1,9 +1,9 @@ module OmniAuth::Strategies - server = Gitlab.config.ldap.servers.first - const_set(server.provider_class, Class.new(LDAP)) + server = Gitlab.config.ldap.servers.values.first + const_set(server['provider_class'], Class.new(LDAP)) end OmniauthCallbacksController.class_eval do - server = Gitlab.config.ldap.servers.first - alias_method server.provider_name, :ldap + server = Gitlab.config.ldap.servers.values.first + alias_method server['provider_name'], :ldap end \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 7770f018a1..226cacfe0d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -205,14 +205,14 @@ Devise.setup do |config| # end if Gitlab.config.ldap.enabled - Gitlab.config.ldap.servers.each do |server| + Gitlab.config.ldap.servers.values.each do |server| if server['allow_username_or_email_login'] email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} else email_stripping_proc = ->(name) {name} end - config.omniauth server.provider_name, + config.omniauth server['provider_name'], host: server['host'], base: server['base'], uid: server['uid'], diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 697b66dcda..d41bfba9b0 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -9,11 +9,11 @@ module Gitlab end def self.servers - Gitlab.config.ldap.servers + Gitlab.config.ldap.servers.values end def self.providers - servers.map &:provider_name + servers.map {|server| server['provider_name'] } end def initialize(provider) @@ -75,7 +75,7 @@ module Gitlab end def config_for(provider) - base_config.servers.find { |server| server.provider_name == provider } + base_config.servers.values.find { |server| server['provider_name'] == provider } end def encryption From 5d59890b9de69a286f833f634f87fc18ddf0db7b Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 14 Oct 2014 13:21:42 +0200 Subject: [PATCH 066/134] Another link to GitHub flow. --- doc/workflow/gitlab_flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 70edea9c8d..f8fd7c97e2 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -26,7 +26,7 @@ After getting used to these three steps the branching model becomes the challeng Since many organizations new to git have no conventions how to work with it, it can quickly become a mess. The biggest problem they run into is that many long running branches that each contain part of the changes are around. People have a hard time figuring out which branch they should develop on or deploy to production. -Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](https://guides.github.com/introduction/flow/index.html) +Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html) We think there is still room for improvement and will detail a set of practices we call GitLab flow. # Git flow and its problems From fedb223b09f001f0a977c9d89130459c417347f2 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 13:52:15 +0200 Subject: [PATCH 067/134] Add valid LDAP server for testing --- config/gitlab.yml.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 59bd144299..4094cbc3eb 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -322,8 +322,7 @@ test: ldap: enabled: false servers: - - - provider_id: main + main: label: ldap host: 127.0.0.1 port: 3890 From 6774d5f6eaaf252215ab7279ead96f38b2e6b0bf Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 14 Oct 2014 16:00:40 +0300 Subject: [PATCH 068/134] Add more stuff to changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 316d7af174..6ddd59df1c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.4.0 - New milestone and label links on issue edit form - Improved repository graphs - Improve event note display in dashboard and project activity views (Vinnie Okada) + - Add users sorting to admin area v 7.3.2 - Fix creating new file via web editor From b4f7b387d0dfaef1766a82040249abb933632930 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Tue, 14 Oct 2014 15:03:53 +0200 Subject: [PATCH 069/134] Explain new configuration options. And add some advertisement to use GitLab EE --- config/gitlab.yml.example | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 4094cbc3eb..260e8c8545 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -135,24 +135,13 @@ production: &base ldap: enabled: false servers: - ## provider id - # - # This identifier is used by GitLab to keep track of which LDAP server each - # GitLab user belongs to. Each LDAP server known to GitLab should have a unique - # provider id. This identifier cannot be changed once users from the LDAP server - # have started logging in to GitLab. - # - # Format: one word, using a-z (lower case) and 0-9 - # Example: 'paris' or 'uswest2' - main: - + main: # 'main' is the GitLab 'provider ID' of this LDAP server ## label # # A human-friendly name for your LDAP server. It is OK to change the label later, # for instance if you find out it is too large to fit on the web page. # # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' host: '_your_ldap_server' @@ -193,6 +182,15 @@ production: &base # user_filter: '' + # GitLab EE only: add more LDAP servers + # Choose an ID made of a-z and 0-9 . This ID will be stored in the database + # so that GitLab can remember which LDAP server a user belongs to. + # uswest2: + # label: + # host: + # .... + + ## OmniAuth settings omniauth: # Allow login via Twitter, Google, etc. using OmniAuth providers From 3c66490e6fda4fcd222de188ace35add821f6b91 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 14 Oct 2014 16:46:50 +0300 Subject: [PATCH 070/134] Use stars icon on explore->starred page Signed-off-by: Dmitriy Zaporozhets --- app/views/explore/projects/_project.html.haml | 1 + app/views/explore/projects/starred.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index 4bc79d0a8c..ffbddbae4d 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -6,6 +6,7 @@ - if current_page?(starred_explore_projects_path) %strong.pull-right + %i.fa.fa-star = pluralize project.star_count, 'star' .project-info diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index d4b1140551..420f069375 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,6 +1,6 @@ .explore-trending-block %p.lead - %i.fa.fa-comments-o + %i.fa.fa-star See most starred projects %hr .public-projects From 30b803fa3f4da65d5b94f582dfa244483bd009ec Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 16:01:45 +0200 Subject: [PATCH 071/134] Add notifications documentation. --- doc/workflow/README.md | 1 + doc/workflow/notifications.md | 71 ++++++++++++++++++++++++ doc/workflow/notifications/settings.png | Bin 0 -> 114727 bytes 3 files changed, 72 insertions(+) create mode 100644 doc/workflow/notifications.md create mode 100644 doc/workflow/notifications/settings.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 323ee48f3b..06490ad404 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -4,3 +4,4 @@ - [Groups](groups.md) - [Labels](labels.md) - [GitLab Flow](gitlab_flow.md) +- [Notifications](notifications.md) diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md new file mode 100644 index 0000000000..a64f30d5de --- /dev/null +++ b/doc/workflow/notifications.md @@ -0,0 +1,71 @@ +# GitLab Notifications + +GitLab has a notifications system in place to notify a user of events important for the workflow. + +## Notification settings + +Under user profile page you can find the notification settings. + +![notification settings](notifications/settings.png) + +We can divide the notification settings into three groups: + +* Global Settings +* Group Settings +* Project Settings + +Each of these settings have levels of notification: + +* Disabled - turns off notifications +* Participating - receive notifications from related resources +* Watch - receive notifications from projects or groups user is a member of +* Global - notifications as set at the global settings + +#### Global Settings + +Global Settings are at the bottom of the hierarchy. + +Any setting set here will be overriden by a setting at the group or a project level. +Group or Project setting can use `global` notification setting which will then use +anything that is set at Global Settings. + +#### Group Settings + +Group Settings are taking presedence to Global Settings but are on a level below Project Settings. +This means that you can set a different level of notifications per group while still being able +to have a finer level setting per project. +Organization like this is suitable for users that belong to different groups but don't have the +same need for being notified for every group they are member of. + +#### Project Settings + +Project Settings are at the top level and any setting placed at this level will take presedence of any +other setting. +This is suitable for users that have different needs for notifications per project basis. + +## Notification events + +Below is the table of events users can be notified of: + +| Event | Sent to | Settings level | +|------------------------------|-------------------------------------------------------------------|------------------------------| +| New SSH key added | User | Security email, always sent. | +| New email added | User | Security email, always sent. | +| New user created | User | Sent on user creation, except for omniauth (LDAP)| +| New issue created | Issue assignee [1], project members [2] | [1] not disabled, [2] higher than participating | +| User added to project | User | Sent when user is added to project | +| Project access level changed | User | Sent when user project access level is changed | +| User added to group | User | Sent when user is added to group | +| Project moved | Project members [1] | [1] not disabled | +| Group access level changed | User | Sent when user group access level is changed | +| Close issue | Issue author [1], issue assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| Reassign issue | New issue assignee [1], old issue assignee [2] | [1] [2] not disabled | +| Reopen issue | Project members [1] | [1] higher than participating | +| New merge request | MR assignee [1] | [1] not disabled | +| Reassign merge request | New MR assignee [1], old MR assignee [2] | [1] [2] not disabled | +| Close merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| Reopen merge request | Project members [1] | [1] higher than participating | +| Merge merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +| New comment | Mentioned users [1], users participating [2], project members [3] | [1] [2] not disabled, [3] higher than participating | + + diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..e5b50ee249478f8d5cd3601e801fe7b7d93d10c2 GIT binary patch literal 114727 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 UU&3@Wo56p|3hGyLmjD0& literal 0 HcmV?d00001 From 8e7da6455b47882b97257ba7266eaf171cb029c8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 14 Oct 2014 16:12:22 +0200 Subject: [PATCH 072/134] Fix spelling mistakes in notifications document. --- doc/workflow/notifications.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index a64f30d5de..3c3ce162df 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -1,6 +1,6 @@ # GitLab Notifications -GitLab has a notifications system in place to notify a user of events important for the workflow. +GitLab has notifications system in place to notify a user of events important for the workflow. ## Notification settings @@ -8,7 +8,7 @@ Under user profile page you can find the notification settings. ![notification settings](notifications/settings.png) -We can divide the notification settings into three groups: +Notification settings are divided into three groups: * Global Settings * Group Settings @@ -24,14 +24,14 @@ Each of these settings have levels of notification: #### Global Settings Global Settings are at the bottom of the hierarchy. - Any setting set here will be overriden by a setting at the group or a project level. -Group or Project setting can use `global` notification setting which will then use + +Group or Project settings can use `global` notification setting which will then use anything that is set at Global Settings. #### Group Settings -Group Settings are taking presedence to Global Settings but are on a level below Project Settings. +Group Settings are taking presedence over Global Settings but are on a level below Project Settings. This means that you can set a different level of notifications per group while still being able to have a finer level setting per project. Organization like this is suitable for users that belong to different groups but don't have the From 706ee232c5a4af1556496af991bb62308fff28dc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 14 Oct 2014 17:47:31 +0300 Subject: [PATCH 073/134] Make accept MR widget looks similar to panels Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/sections/merge_requests.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index c8d0cac292..22f20a7df4 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -111,7 +111,8 @@ .ci_widget { padding: 10px 15px; font-size: 15px; - border-bottom: 1px dashed #AAA; + border-bottom: 1px solid #BBB; + color: #777; &.ci-success { color: $bg_success; @@ -143,7 +144,8 @@ padding: 10px 15px; h4 { - margin-top: 0px; + font-size: 20px; + font-weight: normal; } p:last-child { From 0d30b13a7a95b941061a8466aef32e29870aa66d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 14 Oct 2014 17:48:32 +0300 Subject: [PATCH 074/134] Move "modify merge message link" to the right to prevent accidently hiting accept button Signed-off-by: Dmitriy Zaporozhets --- .../merge_requests/show/_mr_accept.html.haml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index 213e14268c..4939ae0399 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -16,15 +16,6 @@ %h4 You can accept this request automatically. .accept-merge-holder.clearfix - .js-toggle-container - %p - You can - %strong= link_to "modify merge commit message", "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" - before accepting merge request - .js-toggle-content.hide - = render 'shared/commit_message_container', params: params, - text: @merge_request.merge_commit_message, - rows: 14, hint: true .accept-group .pull-left = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" @@ -33,6 +24,14 @@ = label_tag :should_remove_source_branch, class: "checkbox" do = check_box_tag :should_remove_source_branch Remove source-branch + .js-toggle-container + %label + %i.fa.fa-edit + = link_to "modify merge commit message", "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" + .js-toggle-content.hide + = render 'shared/commit_message_container', params: params, + text: @merge_request.merge_commit_message, + rows: 14, hint: true %hr .light From 62b322d7b567f1fae2ea8b5a3b0e71a62506e47d Mon Sep 17 00:00:00 2001 From: Kevin Houdebert Date: Tue, 14 Oct 2014 19:07:34 +0200 Subject: [PATCH 075/134] Add Hipchat services API --- CHANGELOG | 1 + doc/api/services.md | 46 ++++++++++++++++++++++++++++++ lib/api/services.rb | 38 ++++++++++++++++++++++-- spec/requests/api/services_spec.rb | 26 +++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 doc/api/services.md diff --git a/CHANGELOG b/CHANGELOG index 316d7af174..192ff41f1f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.4.0 - New milestone and label links on issue edit form - Improved repository graphs - Improve event note display in dashboard and project activity views (Vinnie Okada) + - API: Add support for Hipchat (Kevin Houdebert) v 7.3.2 - Fix creating new file via web editor diff --git a/doc/api/services.md b/doc/api/services.md new file mode 100644 index 0000000000..ab9f9c00c6 --- /dev/null +++ b/doc/api/services.md @@ -0,0 +1,46 @@ +# Services + +## GitLab CI + +### Edit GitLab CI service + +Set GitLab CI service for a project. + +``` +PUT /projects/:id/services/gitlab-ci +``` + +Parameters: + +- `token` (required) - CI project token +- `project_url` (required) - CI project url + +### Delete GitLab CI service + +Delete GitLab CI service settings for a project. + +``` +DELETE /projects/:id/services/gitlab-ci +``` + +## Hipchat + +### Edit Hipchat service + +Set Hipchat service for project. + +``` +PUT /projects/:id/services/hipchat +``` +Parameters: + +- `token` (required) - Hipchat token +- `room` (required) - Hipchat room name + +### Delete Hipchat service + +Delete Hipchat service for a project. + +``` +DELETE /projects/:id/services/hipchat +``` diff --git a/lib/api/services.rb b/lib/api/services.rb index bde502e32e..3ad59cf3ad 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -28,7 +28,7 @@ module API # Delete GitLab CI service settings # # Example Request: - # DELETE /projects/:id/keys/:id + # DELETE /projects/:id/services/gitlab-ci delete ":id/services/gitlab-ci" do if user_project.gitlab_ci_service user_project.gitlab_ci_service.update_attributes( @@ -38,7 +38,41 @@ module API ) end end + + # Set Hipchat service for project + # + # Parameters: + # token (required) - Hipchat token + # room (required) - Hipchat room name + # + # Example Request: + # PUT /projects/:id/services/hipchat + put ':id/services/hipchat' do + required_attributes! [:token, :room] + attrs = attributes_for_keys [:token, :room] + user_project.build_missing_services + + if user_project.hipchat_service.update_attributes( + attrs.merge(active: true)) + true + else + not_found! + end + end + + # Delete Hipchat service settings + # + # Example Request: + # DELETE /projects/:id/services/hipchat + delete ':id/services/hipchat' do + if user_project.hipchat_service + user_project.hipchat_service.update_attributes( + active: false, + token: nil, + room: nil + ) + end + end end end end - diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index f883c9e028..d8282d0696 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -27,4 +27,30 @@ describe API::API, api: true do project.gitlab_ci_service.should be_nil end end + + describe 'PUT /projects/:id/services/hipchat' do + it 'should update hipchat settings' do + put api("/projects/#{project.id}/services/hipchat", user), + token: 'secret-token', room: 'test' + + response.status.should == 200 + project.hipchat_service.should_not be_nil + end + + it 'should return if required fields missing' do + put api("/projects/#{project.id}/services/gitlab-ci", user), + token: 'secret-token', active: true + + response.status.should == 400 + end + end + + describe 'DELETE /projects/:id/services/hipchat' do + it 'should delete hipchat settings' do + delete api("/projects/#{project.id}/services/hipchat", user) + + response.status.should == 200 + project.hipchat_service.should be_nil + end + end end From 0901345d1b4a83f37f281a6229aa115775a3d5c9 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 14 Oct 2014 15:45:44 -0700 Subject: [PATCH 076/134] make sure tables are UTF8 capable As discussed at https://github.com/gitlabhq/gitlabhq/pull/7742#issuecomment-58897445 make sure that tables have correct char set. --- doc/update/7.3-to-7.4.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index 2e1b993aeb..ba3be5e53b 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -26,6 +26,15 @@ SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') # If previous query returned results, copy & run all outputed SQL statements +# Convert all tables to correct character set +SET foreign_key_checks = 0; +SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; + +# If previous query returned results, copy & run all outputed SQL statements + +# turn foreign key checks back on +SET foreign_key_checks = 1; + # Find MySQL users mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; From ace045499a1ac4988401fb5511e7678e6c53108b Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 14 Oct 2014 22:54:30 -0700 Subject: [PATCH 077/134] fix permission issue in upgrade guides --- doc/update/7.2-to-7.3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index 44f3f8f1a3..ebdd4ff60f 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -74,7 +74,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab # Enable Redis socket for default Debian / Ubuntu path echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf # Be sure redis group can write to the socket, enable only if supported (>= redis 2.4.0). - sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf + sudo sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf # Activate the changes to redis.conf sudo service redis-server restart # Add git to the redis group From cec3b6c355279692a6cd9cf3b884517edcea376d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 14 Oct 2014 23:52:45 -0700 Subject: [PATCH 078/134] add missing cleanup step --- doc/update/4.2-to-5.0.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index 897cd0b91f..cde679598f 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -195,6 +195,12 @@ sudo rm -R tmp sudo -u git -H mkdir tmp sudo chmod -R u+rwX tmp/ +# create directory for pids, make sure GitLab can write to it +sudo -u git -H mkdir tmp/pids/ +sudo chmod -R u+rwX tmp/pids/ + +# if you are already running a newer version of GitLab check that installation guide for other tmp folders you need to create + # reboot system sudo reboot From dcf6c26075027c56e733da7db06e5e355b40074e Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Wed, 15 Oct 2014 09:53:16 +0200 Subject: [PATCH 079/134] Only enable LDAP providers if LDAP is enabled --- config/initializers/7_omniauth.rb | 16 +++++++++------- config/initializers/devise.rb | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 7ef5c10da0..b8ac87fbd5 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -1,9 +1,11 @@ -module OmniAuth::Strategies - server = Gitlab.config.ldap.servers.values.first - const_set(server['provider_class'], Class.new(LDAP)) -end +if Gitlab::LDAP::Config.enabled? + module OmniAuth::Strategies + server = Gitlab.config.ldap.servers.values.first + const_set(server['provider_class'], Class.new(LDAP)) + end -OmniauthCallbacksController.class_eval do - server = Gitlab.config.ldap.servers.values.first - alias_method server['provider_name'], :ldap + OmniauthCallbacksController.class_eval do + server = Gitlab.config.ldap.servers.values.first + alias_method server['provider_name'], :ldap + end end \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 226cacfe0d..c6eb3e5103 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -204,7 +204,7 @@ Devise.setup do |config| # manager.default_strategies(scope: :user).unshift :some_external_strategy # end - if Gitlab.config.ldap.enabled + if Gitlab::LDAP::Config.enabled? Gitlab.config.ldap.servers.values.each do |server| if server['allow_username_or_email_login'] email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} From 732e6c3dbb2a249ec486ece5646eb5fbe46e8680 Mon Sep 17 00:00:00 2001 From: Evgeniy Sokovikov Date: Wed, 15 Oct 2014 13:03:27 +0400 Subject: [PATCH 080/134] same rendering for note diff as for usual diff --- app/views/projects/notes/discussions/_diff.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index da71220af1..b4d1cce798 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -21,7 +21,7 @@ - else %td.old_line= raw(line.type == "new" ? " " : line.old_pos) %td.new_line= raw(line.type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw "#{line.text}  " + %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes From 57b38f6f1c8c6a09ad1d6ac47a28776578703c31 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 12:48:37 +0200 Subject: [PATCH 081/134] Describe who can trigger builds and where, add no red tests policy to contributing doc. --- CONTRIBUTING.md | 1 + doc/development/ci_setup.md | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed49080d57..79632240eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,6 +92,7 @@ For examples of feedback on merge requests please look at already [closed merge 1. The change is as small as possible (see the above paragraph for details) 1. Include proper tests and make all tests pass (unless it contains a test exposing a bug in existing code) +1. All tests have to pass, if you suspect it is unrelated to your contribution ask for tests to be restarted. See [this document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart. 1. Initially contains a single commit (please use `git rebase -i` to squash commits) 1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server) 1. Does not break any existing functionality diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index b3e84183a4..d74f4852a3 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -9,15 +9,17 @@ We currently use three CI services to test GitLab: 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | -|---------------------------------------|---------------------------|------------------------|-----------| -| GitLab CE @ MySQL | ✓ | ✓ | | -| GitLab CE @ PostgreSQL | | | ✓ | -| GitLab EE @ MySQL | ✓ | | | -| GitLab CI @ MySQL | ✓ | | | -| GitLab CI @ PostgreSQL | | | ✓ | -| GitLab CI Runner | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | -| GitLab Shell | ✓ | | ✓ | +|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| +| GitLab CE @ MySQL | ✓ | ✓ [Core team can trigger builds](https://gitlab-ce.githost.io/projects/4) | | +| GitLab CE @ PostgreSQL | | | ✓ [Core team can trigger builds](https://semaphoreapp.com/gitlabhq/gitlabhq/branches/master) | +| GitLab EE @ MySQL | ✓ | | | +| GitLab CI @ MySQL | ✓ | | | +| GitLab CI @ PostgreSQL | | | ✓ | +| GitLab CI Runner | ✓ | | ✓ | +| GitLab Shell | ✓ | | ✓ | +| GitLab Shell | ✓ | | ✓ | + +Core team has access to trigger builds if needed for GitLab CE. We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/build_script_gitlab_ce.md) for testing with GitLab CI. From fb05fa859e5f552e8b84f07d741ea7a68b121f80 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 11:35:16 +0200 Subject: [PATCH 082/134] Add a doc on how to migrate from SVN to gitlab. --- doc/workflow/migrating_from_svn.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 doc/workflow/migrating_from_svn.md diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/migrating_from_svn.md new file mode 100644 index 0000000000..7ff157f482 --- /dev/null +++ b/doc/workflow/migrating_from_svn.md @@ -0,0 +1,17 @@ +# Migrating from SVN to GitLab + +SVN stands for Subversion and is a version control system (VCS). +Git is a distributed revision control and source code management (SCM) system. + +There are some major differences between the two, for more information consult your favourite search engine. + +Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at +[git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). + +Apart from the [official git documentation](http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also +user created step by step guide for migrating from SVN to GitLab. + +[Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca). + +## Contribute to this guide +We welcome all contributions that would expand this guide with instructions on how to migrate from other version control systems. From adb64dc2ee32b23cbbf7b1d9eed59cf4f72c4e63 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 11:37:49 +0200 Subject: [PATCH 083/134] Clearer what to contribute. --- doc/workflow/migrating_from_svn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/migrating_from_svn.md index 7ff157f482..bdcb622f97 100644 --- a/doc/workflow/migrating_from_svn.md +++ b/doc/workflow/migrating_from_svn.md @@ -14,4 +14,4 @@ user created step by step guide for migrating from SVN to GitLab. [Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca). ## Contribute to this guide -We welcome all contributions that would expand this guide with instructions on how to migrate from other version control systems. +We welcome all contributions that would expand this guide with instructions on how to migrate from SVN and other version control systems. From 2803d9e0257c08107db5cebb9a5b10b87d6ca358 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 12:53:06 +0200 Subject: [PATCH 084/134] Git is a distributed vcs. --- doc/workflow/migrating_from_svn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/migrating_from_svn.md index bdcb622f97..207e364180 100644 --- a/doc/workflow/migrating_from_svn.md +++ b/doc/workflow/migrating_from_svn.md @@ -1,7 +1,7 @@ # Migrating from SVN to GitLab SVN stands for Subversion and is a version control system (VCS). -Git is a distributed revision control and source code management (SCM) system. +Git is a distributed version control system. There are some major differences between the two, for more information consult your favourite search engine. From 76cde5c0e534e59c7dcd8bc7e096cfb0bf9f2603 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 13:13:42 +0200 Subject: [PATCH 085/134] Add links to the migration doc, make it clear import is only for git repos. --- app/views/projects/import.html.haml | 3 ++- app/views/projects/new.html.haml | 3 ++- doc/workflow/README.md | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/projects/import.html.haml b/app/views/projects/import.html.haml index 1f7fd26c64..4513c89e78 100644 --- a/app/views/projects/import.html.haml +++ b/app/views/projects/import.html.haml @@ -19,12 +19,13 @@ = form_for @project, url: retry_import_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f| .form-group.import-url-data = f.label :import_url, class: 'control-label' do - %span Import existing repo + %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' .bs-callout.bs-callout-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. + For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} .form-actions = f.submit 'Retry import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 6c986050c4..f5cd0f21e0 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -44,13 +44,14 @@ .js-toggle-content.hide .form-group.import-url-data = f.label :import_url, class: 'control-label' do - %span Import existing repo + %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' .bs-callout.bs-callout-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. + For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} %hr .form-group diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 323ee48f3b..c9768a9803 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -4,3 +4,4 @@ - [Groups](groups.md) - [Labels](labels.md) - [GitLab Flow](gitlab_flow.md) +- [Migrating from SVN to GitLab](migrating_from_svn.md) From af609805c5e6a97893cf1a02d8fb043fff183656 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 15 Oct 2014 13:16:03 +0200 Subject: [PATCH 086/134] Use clearer description. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79632240eb..ce454a11a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ For examples of feedback on merge requests please look at already [closed merge 1. The change is as small as possible (see the above paragraph for details) 1. Include proper tests and make all tests pass (unless it contains a test exposing a bug in existing code) -1. All tests have to pass, if you suspect it is unrelated to your contribution ask for tests to be restarted. See [this document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart. +1. All tests have to pass, if you suspect a failing CI build is unrelated to your contribution ask for tests to be restarted. See [the CI setup document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart. 1. Initially contains a single commit (please use `git rebase -i` to squash commits) 1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server) 1. Does not break any existing functionality From c926922ade4f3f8b84cabe95186860e0f8a6a8f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 15 Oct 2014 15:52:39 +0300 Subject: [PATCH 087/134] Show merge in progress message if MR is locked Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/merge_requests/_show.html.haml | 2 +- .../projects/merge_requests/show/_state_widget.html.haml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 947e8f58ae..7b28dd5e7d 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -21,7 +21,7 @@ - content_for :note_actions do - if can?(current_user, :modify_merge_request, @merge_request) - - unless @merge_request.closed? || @merge_request.merged? + - if @merge_request.open? = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index 2b58c865b2..87dad6140b 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -21,6 +21,12 @@ #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} = render "projects/merge_requests/show/remove_source_branch" + - if @merge_request.locked? + %h4 + Merge in progress... + %p + GitLab tries to merge it right now. During this time merge request is locked and can not be closed. + - unless @commits.any? %h4 Nothing to merge %p From e8e022daecd78b25fd7608cf5fd5476397f1db96 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Wed, 15 Oct 2014 16:29:05 +0200 Subject: [PATCH 088/134] Refer to the Omnibus installation from eveerywhere since people link to installation.md directly. --- README.md | 2 +- doc/install/installation.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0461543f2..2c0643cf59 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ## Installation Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/) for the various options. -Since a manual installation is a lot of work and error prone we strongly recommend fast and reliable Omnibus package installation (deb/rpm) on that page. +Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). ## Third-party applications diff --git a/doc/install/installation.md b/doc/install/installation.md index 0d1a8da4d1..821420e863 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,5 +1,9 @@ # Installation +## Consider the Omnibus package installation + +Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). + ## Select Version to Install Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install. In most cases this should be the highest numbered stable branch (example shown below). From b5763e91cdeaba55b3c426129ba3c4f9638c5eb1 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 15 Oct 2014 18:26:15 +0300 Subject: [PATCH 089/134] add gitlab-shell identification --- .gitignore | 1 + GITLAB_SHELL_VERSION | 2 +- .../initializers/gitlab_shell_secret_token.rb | 19 +++++++++++++++++++ lib/api/helpers.rb | 8 ++++++++ lib/api/internal.rb | 4 ++++ spec/requests/api/internal_spec.rb | 14 +++++++++----- 6 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 config/initializers/gitlab_shell_secret_token.rb diff --git a/.gitignore b/.gitignore index 4f77837151..2c6b65b7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ public/assets/ .envrc dump.rdb tags +.gitlab_shell_secret diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 38f77a65b3..e9307ca575 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.0.1 +2.0.2 diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb new file mode 100644 index 0000000000..8d2b771e53 --- /dev/null +++ b/config/initializers/gitlab_shell_secret_token.rb @@ -0,0 +1,19 @@ +# Be sure to restart your server when you modify this file. + +require 'securerandom' + +# Your secret key for verifying the gitlab_shell. + + +secret_file = Rails.root.join('.gitlab_shell_secret') +gitlab_shell_symlink = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') + +unless File.exist? secret_file + # Generate a new token of 16 random hexadecimal characters and store it in secret_file. + token = SecureRandom.hex(16) + File.write(secret_file, token) +end + +if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(gitlab_shell_symlink) + FileUtils.symlink(secret_file, gitlab_shell_symlink) +end \ No newline at end of file diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3262884f6d..027fb20ec4 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -67,6 +67,10 @@ module API unauthorized! unless current_user end + def authenticate_by_gitlab_shell_token! + unauthorized! unless secret_token == params['secret_token'] + end + def authenticated_as_admin! forbidden! unless current_user.is_admin? end @@ -193,5 +197,9 @@ module API abilities end end + + def secret_token + File.read(Rails.root.join('.gitlab_shell_secret')) + end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9ac659f50f..ebf2296097 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,6 +1,10 @@ module API # Internal access API class Internal < Grape::API + before { + authenticate_by_gitlab_shell_token! + } + namespace 'internal' do # Check if git command is allowed to project # diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6df5ef3896..677b149404 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,10 +5,11 @@ describe API::API, api: true do let(:user) { create(:user) } let(:key) { create(:key, user: user) } let(:project) { create(:project) } + let(:secret_token) { File.read Rails.root.join('.gitlab_shell_secret') } describe "GET /internal/check", no_db: true do it do - get api("/internal/check") + get api("/internal/check"), secret_token: secret_token response.status.should == 200 json_response['api_version'].should == API::API.version @@ -17,7 +18,7 @@ describe API::API, api: true do describe "GET /internal/discover" do it do - get(api("/internal/discover"), key_id: key.id) + get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) response.status.should == 200 @@ -159,7 +160,8 @@ describe API::API, api: true do api("/internal/allowed"), key_id: key.id, project: project.path_with_namespace, - action: 'git-upload-pack' + action: 'git-upload-pack', + secret_token: secret_token ) end @@ -169,7 +171,8 @@ describe API::API, api: true do changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', key_id: key.id, project: project.path_with_namespace, - action: 'git-receive-pack' + action: 'git-receive-pack', + secret_token: secret_token ) end @@ -179,7 +182,8 @@ describe API::API, api: true do ref: 'master', key_id: key.id, project: project.path_with_namespace, - action: 'git-upload-archive' + action: 'git-upload-archive', + secret_token: secret_token ) end end From b57ec54fae7f17fad21c564aee1c39625f77ec20 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 15 Oct 2014 20:25:47 +0300 Subject: [PATCH 090/134] Point to correct project on githost.io. --- doc/development/ci_setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index d74f4852a3..bbd4bf6b25 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -4,7 +4,7 @@ This document describes what services we use for testing GitLab and GitLab CI. We currently use three CI services to test GitLab: -1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/2/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) +1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) From c2bcdeb95090e2344b90e5babe5b68dfba064130 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 15 Oct 2014 20:52:39 +0300 Subject: [PATCH 091/134] Make semaphore configuration an ordered list. --- doc/development/ci_setup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index bbd4bf6b25..ee16aedafe 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -25,9 +25,9 @@ We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master # Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) -Language: Ruby -Ruby verion: 2.1.2 -database.yml: pg +- Language: Ruby +- Ruby verion: 2.1.2 +- database.yml: pg Build commands From 5d7e1b6ae2eab12017c6f6cec9ef6cc96bf2666d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Wed, 15 Oct 2014 23:51:53 -0700 Subject: [PATCH 092/134] match latest config from https://cipherli.st/ --- lib/support/nginx/gitlab-ssl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index d3fb467ef2..42431f54b3 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -60,18 +60,16 @@ server { client_max_body_size 20m; ## Strong SSL Security - ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ ssl on; ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs - ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4'; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_session_cache builtin:1000 shared:SSL:10m; - - ssl_prefer_server_ciphers on; + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; ## [WARNING] The following header states that the browser should only communicate ## with your server over a secure connection for the next 24 months. @@ -88,7 +86,7 @@ server { # ssl_stapling_verify on; # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired - # resolver_timeout 10s; + # resolver_timeout 5s; ## [Optional] Generate a stronger DHE parameter: ## cd /etc/ssl/certs From 92c184a57f7698e79288b380cebc68b839afb4f5 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Thu, 16 Oct 2014 11:46:40 +0200 Subject: [PATCH 093/134] Disallow new users from Oauth signup if `allow_single_sign_on` is disabled Because devise will trigger a save, allowing unsaved users to login, behaviour had changed. The current implementation returns a pre-build user, which can be saved without errors. Reported in #1677 --- app/controllers/omniauth_callbacks_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f46b36568f..589f8387b0 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -54,11 +54,15 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController @user.save end - if @user.valid? + # Only allow properly saved users to login. + if @user.persisted? && @user.valid? sign_in_and_redirect(@user.gl_user) - else + elsif @user.gl_user.errors.any? error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return + else + flash[:notice] = "There's no such user!" + redirect_to new_user_session_path end end end From 761c2a64cc651e2239aac65c6ebd81d17f444703 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 16 Oct 2014 14:02:24 +0300 Subject: [PATCH 094/134] Add items to changelog --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6ddd59df1c..0529069832 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,13 @@ v 7.4.0 - Improved repository graphs - Improve event note display in dashboard and project activity views (Vinnie Okada) - Add users sorting to admin area + - UI improvements + - Fix ambiguous sha problem with mentioned commit + - Fixed bug with apostrophe when at mentioning users + - Add active directory ldap option + - Developers can push to wiki repo. Protected branches does not affect wiki repo any more + - Faster rev list + - Fix branch removal v 7.3.2 - Fix creating new file via web editor From 2e485af7b051512f804ae46a81cba480d2eca46f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 16 Oct 2014 12:16:18 +0000 Subject: [PATCH 095/134] bump gitlab-shelle --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e9307ca575..7ec1d6db40 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.0.2 +2.1.0 From 32d0bf3afdd25eaff1f3bd2e80696e39b5b69c35 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 16 Oct 2014 16:50:09 +0300 Subject: [PATCH 096/134] Fix snippets seeds Signed-off-by: Dmitriy Zaporozhets --- db/fixtures/development/12_snippets.rb | 34 ++++++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index ff91e8430a..b3a6f39c7d 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -1,9 +1,26 @@ Gitlab::Seeder.quiet do - contents = [ - `curl https://gist.githubusercontent.com/randx/4275756/raw/da2f262920c96d1a970d48bf2e99147954b1f4bd/glus1204.sh`, - `curl https://gist.githubusercontent.com/randx/3754594/raw/11026a295e6ef3a151c635707a3e1e8e15fc4725/gitlab_setup.sh`, - `curl https://gist.githubusercontent.com/randx/3065552/raw/29fbd09f4605a5ea22a5a9095e35fd1938dea4d6/gistfile1.sh`, - ] + content =< { where(access_level: GUEST) } + scope :reporters, -> { where(access_level: REPORTER) } + scope :developers, -> { where(access_level: DEVELOPER) } + scope :masters, -> { where(access_level: MASTER) } + scope :owners, -> { where(access_level: OWNER) } + + delegate :name, :username, :email, to: :user, prefix: true +end +eos (1..50).each do |i| user = User.all.sample @@ -12,10 +29,11 @@ Gitlab::Seeder.quiet do id: i, author_id: user.id, title: Faker::Lorem.sentence(3), - file_name: Faker::Internet.domain_word + '.sh', - private: [true, false].sample, - content: contents.sample, + file_name: Faker::Internet.domain_word + '.rb', + visibility_level: Gitlab::VisibilityLevel.values.sample, + content: content, }]) + print('.') end end From fad588f2bee102bf4ab090874d041e227d4e2ee4 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Thu, 16 Oct 2014 17:18:40 +0200 Subject: [PATCH 097/134] Remove LDAP save test This is handled within the LDAP class --- spec/lib/gitlab/oauth/user_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index e4e96fd9f4..7c7d6babbf 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -29,16 +29,16 @@ describe Gitlab::OAuth::User do end describe :save do - context "LDAP" do - let(:provider) { 'ldap' } - it "creates a user from LDAP" do - oauth_user.save + let(:provider) { 'twitter' } - expect(gl_user).to be_valid - expect(gl_user.extern_uid).to eql uid - expect(gl_user.provider).to eql 'ldap' - end + it "creates a user from Omniauth" do + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.extern_uid).to eql uid + expect(gl_user.provider).to eql 'twitter' end + end context "twitter" do let(:provider) { 'twitter' } From d9bfebc0e87ef426aea7eb4fdd1338f04b106354 Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Thu, 16 Oct 2014 20:08:30 +0200 Subject: [PATCH 098/134] Add regressiontest to verify allow_single_sign_on setting verification for #1677 Since testing omniauth_callback_controller.rb is very difficult, the logic is moved to the models --- .../omniauth_callbacks_controller.rb | 13 +++++-------- lib/gitlab/oauth/user.rb | 17 ++++++++++++++--- spec/lib/gitlab/oauth/user_spec.rb | 19 ++++++++----------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 589f8387b0..58d1e37f65 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -49,22 +49,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to profile_path else @user = Gitlab::OAuth::User.new(oauth) - - if Gitlab.config.omniauth['allow_single_sign_on'] && @user.new? - @user.save - end + @user.save # Only allow properly saved users to login. if @user.persisted? && @user.valid? sign_in_and_redirect(@user.gl_user) - elsif @user.gl_user.errors.any? + else @user.gl_user.errors.any? error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return - else - flash[:notice] = "There's no such user!" - redirect_to new_user_session_path end end + rescue StandardError + flash[:notice] = "There's no such user!" + redirect_to new_user_session_path end def oauth diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 133445d3d0..18ec63a62a 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -13,7 +13,7 @@ module Gitlab end def persisted? - gl_user.persisted? + gl_user.try(:persisted?) end def new? @@ -21,10 +21,12 @@ module Gitlab end def valid? - gl_user.valid? + gl_user.try(:valid?) end def save + unauthorized_to_create unless gl_user + gl_user.save! log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" gl_user.block if needs_blocking? @@ -36,7 +38,12 @@ module Gitlab end def gl_user - @user ||= find_by_uid_and_provider || build_new_user + @user ||= find_by_uid_and_provider + + if Gitlab.config.omniauth.allow_single_sign_on + @user ||= build_new_user + end + @user end protected @@ -77,6 +84,10 @@ module Gitlab def model ::User end + + def raise_unauthorized_to_create + raise StandardError.new("Unauthorized to create user, signup disabled for #{auth_hash.provider}") + end end end end diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index 7c7d6babbf..e004d6edfa 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -31,17 +31,8 @@ describe Gitlab::OAuth::User do describe :save do let(:provider) { 'twitter' } - it "creates a user from Omniauth" do - oauth_user.save - - expect(gl_user).to be_valid - expect(gl_user.extern_uid).to eql uid - expect(gl_user.provider).to eql 'twitter' - end - end - - context "twitter" do - let(:provider) { 'twitter' } + context "with allow_single_sign_on enabled" do + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } it "creates a user from Omniauth" do oauth_user.save @@ -51,5 +42,11 @@ describe Gitlab::OAuth::User do expect(gl_user.provider).to eql 'twitter' end end + + context "with allow_single_sign_on disabled (Default)" do + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end + end end end From 077fc683faa85f9abe4cc40ea1c7877e6b6c1f2a Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 16 Oct 2014 14:34:03 -0700 Subject: [PATCH 099/134] simplify DHE parameter generation --- lib/support/nginx/gitlab-ssl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index d3fb467ef2..fd4f93c2f9 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -91,8 +91,7 @@ server { # resolver_timeout 10s; ## [Optional] Generate a stronger DHE parameter: - ## cd /etc/ssl/certs - ## sudo openssl dhparam -out dhparam.pem 4096 + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 ## # ssl_dhparam /etc/ssl/certs/dhparam.pem; From 966f68b33e1f15f08e383ec68346ed1bd690b59b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 17 Oct 2014 13:15:59 +0300 Subject: [PATCH 100/134] Refactor error message a bit Signed-off-by: Dmitriy Zaporozhets --- app/controllers/omniauth_callbacks_controller.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 58d1e37f65..bd4b310fcb 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -54,8 +54,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Only allow properly saved users to login. if @user.persisted? && @user.valid? sign_in_and_redirect(@user.gl_user) - else @user.gl_user.errors.any? - error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") + else + error_message = + if @user.gl_user.errors.any? + @user.gl_user.errors.map do |attribute, message| + "#{attribute} #{message}" + end.join(", ") + else + '' + end + redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end From 4da88dbb2d8d8c1201a18829e22e71837e39736e Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 16 Oct 2014 19:45:33 +0300 Subject: [PATCH 101/134] documents updated --- VERSION | 2 +- doc/install/installation.md | 8 +- ...-or-7.x-to-7.3.md => 6.x-or-7.x-to-7.4.md} | 20 +-- doc/update/7.3-to-7.4.md | 148 +++++++++++++++++- 4 files changed, 159 insertions(+), 19 deletions(-) rename doc/update/{6.x-or-7.x-to-7.3.md => 6.x-or-7.x-to-7.4.md} (93%) diff --git a/VERSION b/VERSION index 8b25872987..7b65f139cb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.4.0-pre +7.4.0.rc1 diff --git a/doc/install/installation.md b/doc/install/installation.md index 821420e863..7a39f2eec9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -74,8 +74,8 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.0.0.tar.gz | tar xz - cd git-2.0.0/ + curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.1.2.tar.gz | tar xz + cd git-2.1.2/ make prefix=/usr/local all # Install into /usr/local/bin @@ -165,9 +165,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-3-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-4-stable gitlab -**Note:** You can change `7-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/6.x-or-7.x-to-7.3.md b/doc/update/6.x-or-7.x-to-7.4.md similarity index 93% rename from doc/update/6.x-or-7.x-to-7.3.md rename to doc/update/6.x-or-7.x-to-7.4.md index fe3530ef9c..e923060223 100644 --- a/doc/update/6.x-or-7.x-to-7.3.md +++ b/doc/update/6.x-or-7.x-to-7.4.md @@ -1,6 +1,6 @@ -# From 6.x or 7.x to 7.3 +# From 6.x or 7.x to 7.4 -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.3. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.4. ## Global issue numbers @@ -70,7 +70,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-3-stable +sudo -u git -H git checkout 7-4-stable ``` OR @@ -78,7 +78,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-3-stable-ee +sudo -u git -H git checkout 7-4-stable-ee ``` ## 4. Install additional packages @@ -152,14 +152,14 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-3-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-4-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.0.0/config.yml.example but with your settings. -* HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab-ssl but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.0.1/config.yml.example but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl but with your settings. * Copy rack attack middleware config ```bash diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index ba3be5e53b..193f44bb67 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -1,14 +1,135 @@ # From 7.3 to 7.4 -## GitLab 7.4 has not been released yet! +### 0. Backup -This document currently just serves as a place to keep track of updates that will be needed for the 7.4 update. +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` -## Update config files +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Get latest code + +```bash +cd /home/git/gitlab +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-4-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-4-stable-ee +``` + +### 3. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + + +### 4. Configure Redis to use sockets + + # Configure redis to use sockets + sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig + # Disable Redis listening on TCP by setting 'port' to 0 + sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf + # Enable Redis socket for default Debian / Ubuntu path + echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf + # Be sure redis group can write to the socket, enable only if supported (>= redis 2.4.0). + sed -i '/# unixsocketperm/ s/^# unixsocketperm.*/unixsocketperm 0775/' /etc/redis/redis.conf + # Activate the changes to redis.conf + sudo service redis-server restart + # Add git to the redis group + sudo usermod -aG redis git + + # Configure Redis connection settings + sudo -u git -H cp config/resque.yml.example config/resque.yml + # Change the Redis socket path if you are not using the default Debian / Ubuntu configuration + sudo -u git -H editor config/resque.yml + + # Configure gitlab-shell to use Redis sockets + sudo -u git -H sed -i 's|^ # socket.*| socket: /var/run/redis/redis.sock|' /home/git/gitlab-shell/config.yml + +### 5. Update config files + +#### New configuration options for gitlab.yml + +There are new configuration options available for gitlab.yml. View them with the command below and apply them to your current gitlab.yml. + +``` +git diff origin/7-3-stable:config/gitlab.yml.example origin/7-4-stable:config/gitlab.yml.example +``` + +#### Change timeout for unicorn + +``` +# config/unicorn.rb +timeout 60 +``` + +#### Change nginx https settings + +* HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl but with your setting + +#### Update database.yml config file(for mysql only) if needed (basically it is required for old gitlab installations) * Add `collation: utf8_general_ci` to config/database.yml as seen in [config/database.yml.mysql](config/database.yml.mysql) -## Optional optimizations for GitLab setups with MySQL databases + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. Update OmniAuth configuration + +When using Google omniauth login, changes of the Google account required. +Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/). +More details can be found at the [integration documentation](../integration/google.md). + +### 9. Optional optimizations for GitLab setups with MySQL databases Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure make a current MySQL database backup beforehand. @@ -75,3 +196,22 @@ mysql> \q # Set production -> password: the password your replaced $password with earlier sudo -u git -H editor /home/git/gitlab/config/database.yml ``` + + +## Things went south? Revert to previous version (7.3) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.2 to 7.3](7.2-to-7.3.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. + + + + From f8cdd62e2269b6c8243b6d1bc9bf73dc7dd1b535 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 17 Oct 2014 14:08:02 +0300 Subject: [PATCH 102/134] Fix account existing blocking Signed-off-by: Dmitriy Zaporozhets --- lib/gitlab/oauth/user.rb | 32 +++++++++---- spec/lib/gitlab/oauth/user_spec.rb | 76 ++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 18ec63a62a..47f62153a5 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -17,7 +17,7 @@ module Gitlab end def new? - !gl_user.persisted? + !persisted? end def valid? @@ -27,10 +27,14 @@ module Gitlab def save unauthorized_to_create unless gl_user - gl_user.save! - log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" - gl_user.block if needs_blocking? + if needs_blocking? + gl_user.save! + gl_user.block + else + gl_user.save! + end + log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" gl_user rescue ActiveRecord::RecordInvalid => e log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" @@ -40,13 +44,27 @@ module Gitlab def gl_user @user ||= find_by_uid_and_provider - if Gitlab.config.omniauth.allow_single_sign_on + if signup_enabled? @user ||= build_new_user end + @user end protected + + def needs_blocking? + new? && block_after_signup? + end + + def signup_enabled? + Gitlab.config.omniauth.allow_single_sign_on + end + + def block_after_signup? + Gitlab.config.omniauth.block_auto_created_users + end + def auth_hash=(auth_hash) @auth_hash = AuthHash.new(auth_hash) end @@ -77,10 +95,6 @@ module Gitlab Gitlab::AppLogger end - def needs_blocking? - Gitlab.config.omniauth['block_auto_created_users'] - end - def model ::User end diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index e004d6edfa..8a83a1b258 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -31,21 +31,77 @@ describe Gitlab::OAuth::User do describe :save do let(:provider) { 'twitter' } - context "with allow_single_sign_on enabled" do - before { Gitlab.config.omniauth.stub allow_single_sign_on: true } + describe 'signup' do + context "with allow_single_sign_on enabled" do + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - it "creates a user from Omniauth" do - oauth_user.save + it "creates a user from Omniauth" do + oauth_user.save - expect(gl_user).to be_valid - expect(gl_user.extern_uid).to eql uid - expect(gl_user.provider).to eql 'twitter' + expect(gl_user).to be_valid + expect(gl_user.extern_uid).to eql uid + expect(gl_user.provider).to eql 'twitter' + end + end + + context "with allow_single_sign_on disabled (Default)" do + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end end end - context "with allow_single_sign_on disabled (Default)" do - it "throws an error" do - expect{ oauth_user.save }.to raise_error StandardError + describe 'blocking' do + let(:provider) { 'twitter' } + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } + + context 'signup' do + context 'dont block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: false } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end + + context 'block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: true } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should be_blocked + end + end + end + + context 'sign-in' do + before do + oauth_user.save + oauth_user.gl_user.activate + end + + context 'dont block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: false } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end + + context 'block on create' do + before { Gitlab.config.omniauth.stub block_auto_created_users: true } + + it do + oauth_user.save + gl_user.should be_valid + gl_user.should_not be_blocked + end + end end end end From 6cff68fb30ef63af127a27293688e4fc40a9cef9 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Fri, 17 Oct 2014 16:27:44 +0200 Subject: [PATCH 103/134] Link to trending public projects so more relevant projects are shown to new users. --- app/views/dashboard/_zero_authorized_projects.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 711e607f0b..5d133cd828 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -46,5 +46,5 @@ %br Public projects are an easy way to allow everyone to have read-only access. .link_holder - = link_to explore_projects_path, class: "btn btn-new" do + = link_to trending_explore_projects_path, class: "btn btn-new" do Browse public projects » From d1c3864778d06b8e47b478caf4ff6f61c573151e Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Fri, 17 Oct 2014 18:03:34 +0200 Subject: [PATCH 104/134] Prevent redeclaration of LDAP strategy --- config/initializers/7_omniauth.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index b8ac87fbd5..18759f0cfb 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -1,7 +1,8 @@ if Gitlab::LDAP::Config.enabled? module OmniAuth::Strategies server = Gitlab.config.ldap.servers.values.first - const_set(server['provider_class'], Class.new(LDAP)) + klass = server['provider_class'] + const_set(klass, Class.new(LDAP)) unless klass == 'LDAP' end OmniauthCallbacksController.class_eval do From 61d9d4e2eb2a51243276422d901b158abbb2f0da Mon Sep 17 00:00:00 2001 From: Jan-Willem van der Meer Date: Fri, 17 Oct 2014 18:08:26 +0200 Subject: [PATCH 105/134] Default the LDAP server label to LDAP --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7e7c91ced7..88cbaefea7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -61,7 +61,6 @@ Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? if Settings.ldap['enabled'] || Rails.env.test? if Settings.ldap['host'].present? server = Settings.ldap.except('sync_time') - server['label'] = 'LDAP' server['provider_name'] = 'ldap' Settings.ldap['servers'] = { 'ldap' => server @@ -69,6 +68,7 @@ if Settings.ldap['enabled'] || Rails.env.test? end Settings.ldap['servers'].each do |key, server| + server['label'] ||= 'LDAP' server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? server['provider_name'] ||= "ldap#{key}".downcase From 6797c59e6e5ceac9f087a63b62fbc148e7a7d5b9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 17 Oct 2014 20:27:30 +0300 Subject: [PATCH 106/134] Improve visual detection of CI status Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/sections/merge_requests.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 22f20a7df4..ec844cc00b 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -113,30 +113,36 @@ font-size: 15px; border-bottom: 1px solid #BBB; color: #777; + background-color: #F5F5F5; &.ci-success { color: $bg_success; border-color: $border_success; + background-color: #F1FAF1; } &.ci-pending { color: #548; border-color: #548; + background-color: #F4F1FA; } &.ci-running { color: $bg_warning; border-color: $border_warning; + background-color: #FAF5F1; } &.ci-failed { color: $bg_danger; border-color: $border_danger; + background-color: #FAF1F1; } &.ci-error { color: $bg_danger; border-color: $border_danger; + background-color: #FAF1F1; } } From 9e6e0171ce4262f10d45ee828773f01fa372cf45 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 17 Oct 2014 20:41:27 +0300 Subject: [PATCH 107/134] Increase participants block margin Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/sections/issues.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index a7fa715d2e..ebf8a6125c 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -75,7 +75,7 @@ } .participants { - margin-bottom: 10px; + margin-bottom: 20px; } .issues_bulk_update { From 290104219652592a221bfe100a7bbbbee69390fb Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sat, 18 Oct 2014 22:36:00 +0200 Subject: [PATCH 108/134] Replace match with end_with: more readable, faster --- lib/tasks/gitlab/shell.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index a8f26a7c02..c3d1aa0125 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -11,7 +11,7 @@ namespace :gitlab do home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Settings.gitlab.user_home gitlab_url = Settings.gitlab.url # gitlab-shell requires a / at the end of the url - gitlab_url += "/" unless gitlab_url.match(/\/$/) + gitlab_url += '/' unless gitlab_url.end_with?('/') repos_path = Gitlab.config.gitlab_shell.repos_path target_dir = Gitlab.config.gitlab_shell.path From f808ecf11e5e5664b9617112691efece5ed01980 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 19 Oct 2014 16:24:22 +0200 Subject: [PATCH 109/134] DRY mentioned in magic note constant --- app/models/note.rb | 16 ++++++++++++++-- app/services/notification_service.rb | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 6f1b1a4da9..f0ed7580b4 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -80,7 +80,7 @@ class Note < ActiveRecord::Base note_options = { project: project, author: author, - note: "_mentioned in #{gfm_reference}_", + note: cross_reference_note_content(gfm_reference), system: true } @@ -174,7 +174,7 @@ class Note < ActiveRecord::Base where(noteable_id: noteable.id) end - notes.where('note like ?', "_mentioned in #{gfm_reference}_"). + notes.where('note like ?', cross_reference_note_content(gfm_reference)). system.any? end @@ -182,8 +182,16 @@ class Note < ActiveRecord::Base where("note like :query", query: "%#{query}%") end + def cross_reference_note_prefix + '_mentioned in ' + end + private + def cross_reference_note_content(gfm_reference) + cross_reference_note_prefix + "#{gfm_reference}_" + end + # Prepend the mentioner's namespaced project path to the GFM reference for # cross-project references. For same-project references, return the # unmodified GFM reference. @@ -249,6 +257,10 @@ class Note < ActiveRecord::Base nil end + def cross_reference? + note.start_with?(self.class.cross_reference_note_prefix) + end + def find_diff return nil unless noteable && noteable.diffs.present? diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index fe39f83b40..3678131427 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -119,7 +119,7 @@ class NotificationService # ignore gitlab service messages return true if note.note =~ /\A_Status changed to closed_/ - return true if note.note =~ /\A_mentioned in / && note.system == true + return true if note.cross_reference? && note.system == true opts = { noteable_type: note.noteable_type, project_id: note.project_id } From 6a73b76c5f2e1559ba771e5910852ac4e1283b58 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 19 Oct 2014 23:02:19 +0200 Subject: [PATCH 110/134] Remove param[:project_id] at admin controller The route never passes that parameter to the helpers. --- app/controllers/admin/projects_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 2f0d344802..bdb11e9bee 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -31,9 +31,7 @@ class Admin::ProjectsController < Admin::ApplicationController protected def project - id = params[:project_id] || params[:id] - - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace(params[:id]) @project || render_404 end From 9e1b97ad99b239ace4a9383ef9d2bf0855c0dfd7 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 19 Oct 2014 23:20:55 +0200 Subject: [PATCH 111/134] Use @project on controllers, don't call method Also memoize the method to ensure that other methods in ApplicationController that rely on it can call it efficiently. --- app/controllers/admin/projects_controller.rb | 4 +- app/controllers/application_controller.rb | 39 ++++++++++--------- app/controllers/projects/commit_controller.rb | 12 +++--- .../projects/deploy_keys_controller.rb | 2 +- .../projects/team_members_controller.rb | 8 ++-- app/controllers/projects_controller.rb | 12 +++--- 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 2f0d344802..51193b91d2 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -38,10 +38,10 @@ class Admin::ProjectsController < Admin::ApplicationController end def group - @group ||= project.group + @group ||= @project.group end def repository - @repository ||= project.repository + @repository ||= @project.repository end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13d8d2a3e0..955f3a14af 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -81,28 +81,31 @@ class ApplicationController < ActionController::Base end def project - id = params[:project_id] || params[:id] + unless @project + id = params[:project_id] || params[:id] - # Redirect from - # localhost/group/project.git - # to - # localhost/group/project - # - if id =~ /\.git\Z/ - redirect_to request.original_url.gsub(/\.git\Z/, '') and return - end + # Redirect from + # localhost/group/project.git + # to + # localhost/group/project + # + if id =~ /\.git\Z/ + redirect_to request.original_url.gsub(/\.git\Z/, '') and return + end - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace(id) - if @project and can?(current_user, :read_project, @project) - @project - elsif current_user.nil? - @project = nil - authenticate_user! - else - @project = nil - render_404 and return + if @project and can?(current_user, :read_project, @project) + @project + elsif current_user.nil? + @project = nil + authenticate_user! + else + @project = nil + render_404 and return + end end + @project end def repository diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 66c67b661d..df09ee7ed9 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -11,12 +11,12 @@ class Projects::CommitController < Projects::ApplicationController def show return git_not_found! unless @commit - @line_notes = project.notes.for_commit_id(commit.id).inline - @branches = project.repository.branch_names_contains(commit.id) + @line_notes = @project.notes.for_commit_id(commit.id).inline + @branches = @project.repository.branch_names_contains(commit.id) @diffs = @commit.diffs - @note = project.build_commit_note(commit) - @notes_count = project.notes.for_commit_id(commit.id).count - @notes = project.notes.for_commit_id(@commit.id).not_inline.fresh + @note = @project.build_commit_note(commit) + @notes_count = @project.notes.for_commit_id(commit.id).count + @notes = @project.notes.for_commit_id(@commit.id).not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @comments_target = { @@ -32,6 +32,6 @@ class Projects::CommitController < Projects::ApplicationController end def commit - @commit ||= project.repository.commit(params[:id]) + @commit ||= @project.repository.commit(params[:id]) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index d20937ea8e..024b9520d3 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -42,7 +42,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def enable - project.deploy_keys << available_keys.find(params[:id]) + @project.deploy_keys << available_keys.find(params[:id]) redirect_to project_deploy_keys_path(@project) end diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb index 7bb799eba6..0791e6080f 100644 --- a/app/controllers/projects/team_members_controller.rb +++ b/app/controllers/projects/team_members_controller.rb @@ -10,7 +10,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def new - @user_project_relation = project.project_members.new + @user_project_relation = @project.project_members.new end def create @@ -26,7 +26,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def update - @user_project_relation = project.project_members.find_by(user_id: member) + @user_project_relation = @project.project_members.find_by(user_id: member) @user_project_relation.update_attributes(member_params) unless @user_project_relation.valid? @@ -36,7 +36,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def destroy - @user_project_relation = project.project_members.find_by(user_id: member) + @user_project_relation = @project.project_members.find_by(user_id: member) @user_project_relation.destroy respond_to do |format| @@ -46,7 +46,7 @@ class Projects::TeamMembersController < Projects::ApplicationController end def leave - project.project_members.find_by(user_id: current_user).destroy + @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| format.html { redirect_to :back } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b3380a6ff2..75495a3c3a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -76,7 +76,7 @@ class ProjectsController < ApplicationController end def import - if project.import_finished? + if @project.import_finished? redirect_to @project return end @@ -98,7 +98,7 @@ class ProjectsController < ApplicationController end def destroy - return access_denied! unless can?(current_user, :remove_project, project) + return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute @@ -148,8 +148,8 @@ class ProjectsController < ApplicationController end def archive - return access_denied! unless can?(current_user, :archive_project, project) - project.archive! + return access_denied! unless can?(current_user, :archive_project, @project) + @project.archive! respond_to do |format| format.html { redirect_to @project } @@ -157,8 +157,8 @@ class ProjectsController < ApplicationController end def unarchive - return access_denied! unless can?(current_user, :archive_project, project) - project.unarchive! + return access_denied! unless can?(current_user, :archive_project, @project) + @project.unarchive! respond_to do |format| format.html { redirect_to @project } From 8ad1330b6a8648406bcd392ad5884498a25fbceb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 20 Oct 2014 10:52:29 +0200 Subject: [PATCH 112/134] Ask the wiki repo, not Gollum, if it's empty We need to skip empty repositories when creating a backup. Before this change, we were asking gollum-lib if the wiki contains any _pages_. Now we ask gitlab_git if the repository contains _files_. This should resolve gollum_lib Grit timeouts in the backup script. --- lib/backup/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 4e99d4bbe5..380beac708 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -30,7 +30,7 @@ module Backup if File.exists?(path_to_repo(wiki)) print " * #{wiki.path_with_namespace} ... " - if wiki.empty? + if wiki.repository.empty? puts " [SKIPPED]".cyan else output, status = Gitlab::Popen.popen(%W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)) From f50c0e5af11c07b637c84d95622a315b71eebe97 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 20 Oct 2014 12:59:16 +0300 Subject: [PATCH 113/134] Fix group user removal from admin area Signed-off-by: Dmitriy Zaporozhets --- app/views/admin/groups/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index c1a9214b77..1da6e4c5f1 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -80,7 +80,7 @@ = link_to user.name, admin_user_path(user) %span.pull-right.light = member.human_access - = link_to group_group_members_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' From 2064a147249ab5984d90980bb9ce44f788a9b941 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 20 Oct 2014 13:18:07 +0300 Subject: [PATCH 114/134] Add tests for remove group member feature in admin area Signed-off-by: Dmitriy Zaporozhets --- .../groups/group_members_controller.rb | 1 + app/views/admin/groups/show.html.haml | 2 +- features/admin/groups.feature | 7 ++++++ features/steps/admin/groups.rb | 23 +++++++++++++++++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 63c05d4f33..ca88d03387 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -19,6 +19,7 @@ class Groups::GroupMembersController < ApplicationController def destroy @users_group = @group.group_members.find(params[:id]) + if can?(current_user, :destroy, @users_group) # May fail if last owner. @users_group.destroy respond_to do |format| diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 1da6e4c5f1..4494acc484 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -74,7 +74,7 @@ %ul.well-list.group-users-list - @members.each do |member| - user = member.user - %li{class: dom_class(user)} + %li{class: dom_class(member), id: dom_id(user)} .list-item-name %strong = link_to user.name, admin_user_path(user) diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 1a465c1be5..aa365a6ea1 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -20,3 +20,10 @@ Feature: Admin Groups When I visit admin group page When I select user "John Doe" from user list as "Reporter" Then I should see "John Doe" in team list in every project as "Reporter" + + @javascript + Scenario: Remove user from group + Given we have user "John Doe" in group + When I visit admin group page + And I remove user "John Doe" from group + Then I should not see "John Doe" in team list diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 4f0ba05606..d69a87cd07 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -37,8 +37,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end When 'I select user "John Doe" from user list as "Reporter"' do - user = User.find_by(name: "John Doe") - select2(user.id, from: "#user_ids", multiple: true) + select2(user_john.id, from: "#user_ids", multiple: true) within "#new_team_member" do select "Reporter", from: "access_level" end @@ -58,9 +57,29 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end end + step 'we have user "John Doe" in group' do + current_group.add_user(user_john, Gitlab::Access::REPORTER) + end + + step 'I remove user "John Doe" from group' do + within "#user_#{user_john.id}" do + click_link 'Remove user from group' + end + end + + step 'I should not see "John Doe" in team list' do + within ".group-users-list" do + page.should_not have_content "John Doe" + end + end + protected def current_group @group ||= Group.first end + + def user_john + @user_john ||= User.find_by(name: "John Doe") + end end From c0a5d043819d6ad52912e2ba68c7fcb12e237978 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 20 Oct 2014 13:51:02 +0300 Subject: [PATCH 115/134] 7.5.0 started Signed-off-by: Dmitriy Zaporozhets --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7b65f139cb..027a8b7b33 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.4.0.rc1 +7.5.0.pre From 1768e3eccb91689405e411a3ebcb2622e16dcdd8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 20 Oct 2014 16:18:45 +0200 Subject: [PATCH 116/134] Update the documentation for the LDAP user filter --- doc/integration/ldap.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index ee472ac3e3..a89c2d3877 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -26,13 +26,20 @@ The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515). ```ruby # For omnibus-gitlab gitlab_rails['ldap_user_filter'] = '(employeeType=developer)' +gitlab_rails['ldap_servers'] = YAML.load <<-EOS +main: + # snip... + user_filter: '(employeeType=developer)' +EOS ``` ```yaml # For installations from source production: ldap: - user_filter: '(employeeType=developer)' + servers: + main: + user_filter: '(employeeType=developer)' ``` Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: From 0b78bd7a42a341bb227fb544b5b12abf8e152a41 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 20 Oct 2014 16:22:36 +0200 Subject: [PATCH 117/134] Keep the legacy LDAP syntax in the documentation --- doc/integration/ldap.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index a89c2d3877..869850d29d 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -24,22 +24,31 @@ If you want to limit all GitLab access to a subset of the LDAP users on your LDA The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515). ```ruby -# For omnibus-gitlab -gitlab_rails['ldap_user_filter'] = '(employeeType=developer)' +# For omnibus packages; new LDAP server syntax gitlab_rails['ldap_servers'] = YAML.load <<-EOS main: # snip... user_filter: '(employeeType=developer)' EOS + +# omnibus package; legacy syntax +gitlab_rails['ldap_user_filter'] = '(employeeType=developer)' ``` ```yaml -# For installations from source +# For installations from source; new LDAP server syntax production: ldap: servers: main: + # snip... user_filter: '(employeeType=developer)' + +# installations from source; legacy syntax +production: + ldap: + # snip... + user_filter: '(employeeType=developer)' ``` Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: From 5ed7c20150928194306ad51263b6a9b7fb4b4cfd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 20 Oct 2014 17:22:39 +0300 Subject: [PATCH 118/134] Prevent 500 error when filter projects with push in admin area Signed-off-by: Dmitriy Zaporozhets --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 90d2649ba2..613f98ba44 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -173,7 +173,7 @@ class Project < ActiveRecord::Base end def with_push - includes(:events).where('events.action = ?', Event::PUSHED) + joins(:events).where('events.action = ?', Event::PUSHED) end def active From 46cdb931d8a48febd459cf932b6e7c7b626ec452 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 20 Oct 2014 16:41:28 +0200 Subject: [PATCH 119/134] Remove legacy LDAP configuration examples --- doc/integration/ldap.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 869850d29d..df72b17ab1 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -30,9 +30,6 @@ main: # snip... user_filter: '(employeeType=developer)' EOS - -# omnibus package; legacy syntax -gitlab_rails['ldap_user_filter'] = '(employeeType=developer)' ``` ```yaml @@ -43,12 +40,6 @@ production: main: # snip... user_filter: '(employeeType=developer)' - -# installations from source; legacy syntax -production: - ldap: - # snip... - user_filter: '(employeeType=developer)' ``` Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: From b1b6761e05de0b675e31fb227939fff36618a282 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 20 Oct 2014 16:41:37 +0200 Subject: [PATCH 120/134] Add LDAP configuration documentation --- doc/integration/ldap.md | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index df72b17ab1..56b0d826ad 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,95 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Configuring GitLab for LDAP integration + +To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. +In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server. + +Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration. +The old LDAP integration syntax still works in GitLab 7.4. +If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab. + +```ruby +# For omnibus packages +gitlab_rails['ldap_enabled'] = true +gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below +main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 636 + uid: 'sAMAccountName' + method: 'ssl' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + +# GitLab EE only: add more LDAP servers +# Choose an ID made of a-z and 0-9 . This ID will be stored in the database +# so that GitLab can remember which LDAP server a user belongs to. +# uswest2: +# label: +# host: +# .... +EOS +``` + +If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: + +``` +production: + # snip... + ldap: + enabled: false + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + # snip... +``` + ## Enabling LDAP sign-in for existing GitLab users When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user. From c0c8dccf2e36c269cfb26b31b86cf60d262c4843 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 20 Oct 2014 22:48:07 +0200 Subject: [PATCH 121/134] Export all coffee classes with @ --- app/assets/javascripts/activities.js.coffee | 4 +--- app/assets/javascripts/admin.js.coffee | 4 +--- app/assets/javascripts/blob.js.coffee | 5 +---- app/assets/javascripts/commit.js.coffee | 4 +--- app/assets/javascripts/commit/file.js.coffee | 4 +--- app/assets/javascripts/commit/image-file.js.coffee | 4 +--- app/assets/javascripts/commits.js.coffee | 4 +--- app/assets/javascripts/confirm_danger_modal.js.coffee | 4 +--- app/assets/javascripts/dashboard.js.coffee | 5 +---- app/assets/javascripts/diff.js.coffee | 5 +---- app/assets/javascripts/flash.js.coffee | 4 +--- app/assets/javascripts/groups.js.coffee | 4 +--- app/assets/javascripts/issue.js.coffee | 4 +--- app/assets/javascripts/labels.js.coffee | 4 +--- app/assets/javascripts/merge_request.js.coffee | 4 +--- app/assets/javascripts/milestone.js.coffee | 4 +--- app/assets/javascripts/notes.js.coffee | 6 +----- app/assets/javascripts/notes_votes.js.coffee | 4 +--- app/assets/javascripts/project.js.coffee | 5 +---- app/assets/javascripts/project_import.js.coffee | 4 +--- app/assets/javascripts/search_autocomplete.js.coffee | 4 +--- app/assets/javascripts/stat_graph.js.coffee | 2 +- app/assets/javascripts/stat_graph_contributors.js.coffee | 2 +- .../javascripts/stat_graph_contributors_graph.js.coffee | 6 +++--- app/assets/javascripts/team_members.js.coffee | 4 +--- app/assets/javascripts/tree.js.coffee | 4 +--- app/assets/javascripts/wikis.js.coffee | 5 +---- 27 files changed, 29 insertions(+), 84 deletions(-) diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index fdefbfb92b..4f76d8ce48 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,4 +1,4 @@ -class Activities +class @Activities constructor: -> Pager.init 20, true $(".event_filter_link").bind "click", (event) => @@ -27,5 +27,3 @@ class Activities event_filters.splice index, 1 $.cookie "event_filter", event_filters.join(","), { path: '/' } - -@Activities = Activities diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index a333eed87f..bcb2e6df7c 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -1,4 +1,4 @@ -class Admin +class @Admin constructor: -> $('input#user_force_random_password').on 'change', (elem) -> elems = $('#user_password, #user_password_confirmation') @@ -51,5 +51,3 @@ class Admin $('li.group_member').bind 'ajax:success', -> Turbolinks.visit(location.href) - -@Admin = Admin diff --git a/app/assets/javascripts/blob.js.coffee b/app/assets/javascripts/blob.js.coffee index 9db919e5a6..a5f15f80c5 100644 --- a/app/assets/javascripts/blob.js.coffee +++ b/app/assets/javascripts/blob.js.coffee @@ -1,4 +1,4 @@ -class BlobView +class @BlobView constructor: -> # handle multi-line select handleMultiSelect = (e) -> @@ -71,6 +71,3 @@ class BlobView # Highlight the correct lines when the hash part of the URL changes $(window).on("hashchange", highlightBlobLines) - - -@BlobView = BlobView diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee index 5f53439ca4..0566e23919 100644 --- a/app/assets/javascripts/commit.js.coffee +++ b/app/assets/javascripts/commit.js.coffee @@ -1,6 +1,4 @@ -class Commit +class @Commit constructor: -> $('.files .diff-file').each -> new CommitFile(this) - -@Commit = Commit diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee index 4db9116a9d..83e793863b 100644 --- a/app/assets/javascripts/commit/file.js.coffee +++ b/app/assets/javascripts/commit/file.js.coffee @@ -1,7 +1,5 @@ -class CommitFile +class @CommitFile constructor: (file) -> if $('.image', file).length new ImageFile(file) - -@CommitFile = CommitFile diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee index 607b85eb45..9e5f49b1f6 100644 --- a/app/assets/javascripts/commit/image-file.js.coffee +++ b/app/assets/javascripts/commit/image-file.js.coffee @@ -1,4 +1,4 @@ -class ImageFile +class @ImageFile # Width where images must fits in, for 2-up this gets divided by 2 @availWidth = 900 @@ -124,5 +124,3 @@ class ImageFile else img.on 'load', => callback.call(this, domImg.naturalWidth, domImg.naturalHeight) - -@ImageFile = ImageFile diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index 784d7d20bb..c183e78e51 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,4 +1,4 @@ -class CommitsList +class @CommitsList @data = ref: null limit: 0 @@ -53,5 +53,3 @@ class CommitsList @disable callback: => this.getOld() - -this.CommitsList = CommitsList diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee index 1687b7d961..bb99edbd09 100644 --- a/app/assets/javascripts/confirm_danger_modal.js.coffee +++ b/app/assets/javascripts/confirm_danger_modal.js.coffee @@ -1,4 +1,4 @@ -class ConfirmDangerModal +class @ConfirmDangerModal constructor: (form, text) -> @form = form $('.js-confirm-text').text(text || '') @@ -16,5 +16,3 @@ class ConfirmDangerModal $('.js-confirm-danger-submit').on 'click', => @form.submit() - -@ConfirmDangerModal = ConfirmDangerModal diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index c4a0ccd9c2..6ef5a539b8 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,4 +1,4 @@ -class Dashboard +class @Dashboard constructor: -> @initSidebarTab() @@ -28,6 +28,3 @@ class Dashboard # show tab from cookie sidebar_filter = $.cookie(key) $("#" + sidebar_filter).tab('show') if sidebar_filter - - -@Dashboard = Dashboard diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index dbe00c487d..52b4208524 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -1,4 +1,4 @@ -class Diff +class @Diff UNFOLD_COUNT = 20 constructor: -> $(document).on('click', '.js-unfold', (event) => @@ -41,6 +41,3 @@ class Diff lines = line.children().slice(0, 2) line_numbers = ($(l).attr('data-linenumber') for l in lines) (parseInt(line_number) for line_number in line_numbers) - - -@Diff = Diff diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index cf1a37eae3..b39ab0c447 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,4 +1,4 @@ -class Flash +class @Flash constructor: (message, type)-> flash = $(".flash-container") flash.html("") @@ -10,5 +10,3 @@ class Flash flash.click -> $(@).fadeOut() flash.show() - -@Flash = Flash diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee index 4b1000f9a6..9012204424 100644 --- a/app/assets/javascripts/groups.js.coffee +++ b/app/assets/javascripts/groups.js.coffee @@ -1,10 +1,8 @@ -class GroupMembers +class @GroupMembers constructor: -> $('li.group_member').bind 'ajax:success', -> $(this).fadeOut() -@GroupMembers = GroupMembers - $ -> # avatar $('.js-choose-group-avatar-button').bind "click", -> diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 0e2a2fa792..597b4695a6 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,4 +1,4 @@ -class Issue +class @Issue constructor: -> $('.edit-issue.inline-update input[type="submit"]').hide() $(".issue-box .inline-update").on "change", "select", -> @@ -15,5 +15,3 @@ class Issue "issue" updateTaskState ) - -@Issue = Issue diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee index d306ad64f5..1bc8840f9a 100644 --- a/app/assets/javascripts/labels.js.coffee +++ b/app/assets/javascripts/labels.js.coffee @@ -1,4 +1,4 @@ -class Labels +class @Labels constructor: -> form = $('.label-form') @setupLabelForm(form) @@ -31,5 +31,3 @@ class Labels # Notify the form, that color has changed $('.label-form').trigger('keyup') e.preventDefault() - -@Labels = Labels diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 9f99ff403f..46e06424e5 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,4 +1,4 @@ -class MergeRequest +class @MergeRequest constructor: (@opts) -> @initContextWidget() this.$el = $('.merge-request') @@ -132,5 +132,3 @@ class MergeRequest this.$('.automerge_widget').hide() this.$('.merge-in-progress').hide() this.$('.automerge_widget.already_cannot_be_merged').show() - -this.MergeRequest = MergeRequest diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index ea01c318d4..c42f31933d 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -1,4 +1,4 @@ -class Milestone +class @Milestone @updateIssue: (li, issue_url, data) -> $.ajax type: "PUT" @@ -115,5 +115,3 @@ class Milestone Milestone.updateMergeRequest(ui.item, merge_request_url, data) ).disableSelection() - -@Milestone = Milestone diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ba8d7a9a2f..978f83dd44 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,4 +1,4 @@ -class Notes +class @Notes @interval: null constructor: (notes_url, note_ids, last_fetched_at) -> @@ -514,7 +514,3 @@ class Notes else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') - - - -@Notes = Notes diff --git a/app/assets/javascripts/notes_votes.js.coffee b/app/assets/javascripts/notes_votes.js.coffee index b31eb9ac9d..65c149b788 100644 --- a/app/assets/javascripts/notes_votes.js.coffee +++ b/app/assets/javascripts/notes_votes.js.coffee @@ -1,4 +1,4 @@ -class NotesVotes +class @NotesVotes updateVotes: -> votes = $("#votes .votes") notes = $("#notes-list .note .vote") @@ -18,5 +18,3 @@ class NotesVotes # replace vote numbers votes.find(".upvotes").text votes.find(".upvotes").text().replace(/\d+/, upvotes) votes.find(".downvotes").text votes.find(".downvotes").text().replace(/\d+/, downvotes) - -@NotesVotes = NotesVotes diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index f4a8a178e7..aba40742e5 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,4 +1,4 @@ -class Project +class @Project constructor: -> $('.project-edit-container').on 'ajax:before', => $('.project-edit-container').hide() @@ -24,9 +24,6 @@ class Project else $('#project_issues_tracker_id').removeAttr('disabled') - -@Project = Project - $ -> # Git clone panel switcher scope = $ '.git-clone-holder' diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee index 7cf44da99f..6633564a07 100644 --- a/app/assets/javascripts/project_import.js.coffee +++ b/app/assets/javascripts/project_import.js.coffee @@ -1,7 +1,5 @@ -class ProjectImport +class @ProjectImport constructor: -> setTimeout -> Turbolinks.visit(location.href) , 5000 - -@ProjectImport = ProjectImport diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index e144dfa1d6..c180136526 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,4 +1,4 @@ -class SearchAutocomplete +class @SearchAutocomplete constructor: (search_autocomplete_path, project_id, project_ref) -> project_id = '' unless project_id project_ref = '' unless project_ref @@ -9,5 +9,3 @@ class SearchAutocomplete minLength: 1 select: (event, ui) -> location.href = ui.item.url - -@SearchAutocomplete = SearchAutocomplete diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee index b129619696..f36c71fd25 100644 --- a/app/assets/javascripts/stat_graph.js.coffee +++ b/app/assets/javascripts/stat_graph.js.coffee @@ -1,4 +1,4 @@ -class window.StatGraph +class @StatGraph @log: {} @get_log: -> @log diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index ab785a5454..27f0fd31d5 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,4 +1,4 @@ -class window.ContributorsStatGraph +class @ContributorsStatGraph init: (log) -> @parsed_log = ContributorsStatGraphUtil.parse_log(log) @set_current_field("commits") diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee index 834c7e5dab..9952fa0b00 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -1,4 +1,4 @@ -class window.ContributorsGraph +class @ContributorsGraph MARGIN: top: 20 right: 20 @@ -44,7 +44,7 @@ class window.ContributorsGraph set_data: (data) -> @data = data -class window.ContributorsMasterGraph extends ContributorsGraph +class @ContributorsMasterGraph extends ContributorsGraph constructor: (@data) -> @width = $('.container').width() - 70 @height = 200 @@ -117,7 +117,7 @@ class window.ContributorsMasterGraph extends ContributorsGraph @svg.select("path").attr("d", @area) @svg.select(".y.axis").call(@y_axis) -class window.ContributorsAuthorGraph extends ContributorsGraph +class @ContributorsAuthorGraph extends ContributorsGraph constructor: (@data) -> @width = $('.container').width()/2 - 100 @height = 200 diff --git a/app/assets/javascripts/team_members.js.coffee b/app/assets/javascripts/team_members.js.coffee index 5eaa8ad4ff..32486f7da5 100644 --- a/app/assets/javascripts/team_members.js.coffee +++ b/app/assets/javascripts/team_members.js.coffee @@ -1,6 +1,4 @@ -class TeamMembers +class @TeamMembers constructor: -> $('.team-members .project-access-select').on "change", -> $(this.form).submit() - -@TeamMembers = TeamMembers diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee index 4852e879b6..d428db5b42 100644 --- a/app/assets/javascripts/tree.js.coffee +++ b/app/assets/javascripts/tree.js.coffee @@ -1,4 +1,4 @@ -class TreeView +class @TreeView constructor: -> @initKeyNav() @@ -39,5 +39,3 @@ class TreeView else if e.which is 13 path = $('.tree-item.selected .tree-item-file-name a').attr('href') Turbolinks.visit(path) - -@TreeView = TreeView diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 17e790e5b7..66757565d3 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,4 +1,4 @@ -class Wikis +class @Wikis constructor: -> $('.build-new-wiki').bind "click", -> field = $('#new_wiki_path') @@ -7,6 +7,3 @@ class Wikis if(slug.length > 0) location.href = path + "/" + slug - - -@Wikis = Wikis From f7d01f2067048e2dbdff14ef081a66d89316824c Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 20 Oct 2014 23:43:51 +0200 Subject: [PATCH 122/134] Factor group tips --- app/views/admin/groups/_form.html.haml | 7 +------ app/views/groups/new.html.haml | 7 +------ app/views/shared/_group_tips.html.haml | 6 ++++++ 3 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 app/views/shared/_group_tips.html.haml diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index c56863ce27..7b55249bdc 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -29,12 +29,7 @@ .col-sm-2 .col-sm-10 .bs-callout.bs-callout-info - %ul - %li A group is a collection of several projects - %li Groups are private by default - %li Members of a group may only view projects they have permission to access - %li Group project URLs are prefixed with the group namespace - %li Existing projects may be moved into a group + = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', admin_groups_path, class: "btn btn-cancel" diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 235e299343..ccc17dc436 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,12 +27,7 @@ .form-group .col-sm-2 .col-sm-10 - %ul - %li A group is a collection of several projects - %li Groups are private by default - %li Members of a group may only view projects they have permission to access - %li Group project URLs are prefixed with the group namespace - %li Existing projects may be moved into a group + = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create", tabindex: 3 diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml new file mode 100644 index 0000000000..e5cf783beb --- /dev/null +++ b/app/views/shared/_group_tips.html.haml @@ -0,0 +1,6 @@ +%ul + %li A group is a collection of several projects + %li Groups are private by default + %li Members of a group may only view projects they have permission to access + %li Group project URLs are prefixed with the group namespace + %li Existing projects may be moved into a group From 01db264ffcaaa5579c63b1d33f97d446cc726c3e Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 20 Oct 2014 23:29:49 +0200 Subject: [PATCH 123/134] Factor choose group avatar button --- app/views/admin/groups/_form.html.haml | 8 +------- app/views/groups/edit.html.haml | 8 +------- app/views/groups/new.html.haml | 8 +------- app/views/shared/_choose_group_avatar_button.html.haml | 7 +++++++ 4 files changed, 10 insertions(+), 21 deletions(-) create mode 100644 app/views/shared/_choose_group_avatar_button.html.haml diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index c56863ce27..37ce68adc3 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -16,13 +16,7 @@ .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' .col-sm-10 - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... -   - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f - if @group.new_record? .form-group diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0b15affe78..b40c164f91 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -31,13 +31,7 @@ You can change your group avatar here - else You can upload a group avatar here - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... -   - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f - if @group.avatar? %hr = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 235e299343..df5c954199 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,13 +16,7 @@ .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' .col-sm-10 - %a.choose-btn.btn.btn-small.js-choose-group-avatar-button - %i.fa.fa-paperclip - %span Choose File ... -   - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-group-avatar-input hidden" - .light The maximum file size allowed is 100KB. + = render 'shared/choose_group_avatar_button', f: f .form-group .col-sm-2 diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml new file mode 100644 index 0000000000..f32c2d388a --- /dev/null +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -0,0 +1,7 @@ +%a.choose-btn.btn.btn-small.js-choose-group-avatar-button + %i.fa.fa-paperclip + %span Choose File ... +  +%span.file_name.js-avatar-filename File name... += f.file_field :avatar, class: 'js-group-avatar-input hidden' +.light The maximum file size allowed is 100KB. From 3a47ed979a62e91fdcd7d05ebb309759788a23b6 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Tue, 21 Oct 2014 00:25:44 +0200 Subject: [PATCH 124/134] Factor group forms --- app/views/admin/groups/_form.html.haml | 10 +--------- app/views/groups/edit.html.haml | 11 +---------- app/views/groups/new.html.haml | 10 +--------- app/views/shared/_group_form.html.haml | 12 ++++++++++++ 4 files changed, 15 insertions(+), 28 deletions(-) create mode 100644 app/views/shared/_group_form.html.haml diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index c56863ce27..bc612307de 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,16 +2,8 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group.group_name_holder - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Example Group", class: "form-control" - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4 + = render 'shared/group_form', f: f .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0b15affe78..c2fcace820 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -11,16 +11,7 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Ex. OpenSource", class: "form-control left" - - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4 + = render 'shared/group_form', f: f .form-group .col-sm-2 diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 235e299343..2116d21ac4 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -2,16 +2,8 @@ - if @group.errors.any? .alert.alert-danger %span= @group.errors.full_messages.first - .form-group - = f.label :name, class: 'control-label' do - Group name - .col-sm-10 - = f.text_field :name, placeholder: "Ex. OpenSource", class: "form-control", tabindex: 1, autofocus: true - .form-group.group-description-holder - = f.label :description, "Details", class: 'control-label' - .col-sm-10 - = f.text_area :description, maxlength: 250, class: "form-control js-gfm-input", rows: 4, tabindex: 2 + = render 'shared/group_form', f: f, autofocus: true .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml new file mode 100644 index 0000000000..93294e4250 --- /dev/null +++ b/app/views/shared/_group_form.html.haml @@ -0,0 +1,12 @@ +.form-group + = f.label :name, class: 'control-label' do + Group name + .col-sm-10 + = f.text_field :name, placeholder: 'Example Group', class: 'form-control', + autofocus: local_assigns[:autofocus] || false + +.form-group.group-description-holder + = f.label :description, 'Details', class: 'control-label' + .col-sm-10 + = f.text_area :description, maxlength: 250, + class: 'form-control js-gfm-input', rows: 4 From 19ab9b40b800e15a1a07b00b9adc6534ada12dd1 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 20 Oct 2014 18:03:12 +0200 Subject: [PATCH 125/134] State on CONTRIBUTING fix line style --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce454a11a0..d8d3c25108 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,11 @@ For examples of feedback on merge requests please look at already [closed merge 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options since they complicate future changes 1. Changes after submitting the merge request should be in separate commits (no squashing). You will be asked to squash when the review is over, before merging. -1. It conforms to the following style guides +1. It conforms to the following style guides. + If your change touches a line that does not follow the style, + modify the entire line to follow it. This prevents linting tools from generating warnings. + Don't touch neighbouring lines. As an exception, automatic mass refactoring modifications + may leave style non-compliant. ## Style guides From 2c98584a9c0c019af58012865ae4425df5127ac6 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 19 Oct 2014 11:25:43 +0200 Subject: [PATCH 126/134] Remove unused admin/projects#repository method Already defined on the ApplicationController base class. --- app/controllers/admin/projects_controller.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 23d4a9860a..7c2388e81b 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -38,8 +38,4 @@ class Admin::ProjectsController < Admin::ApplicationController def group @group ||= @project.group end - - def repository - @repository ||= @project.repository - end end From 593a287c8d0cfcc22ca2db35dc9a72140e296c2e Mon Sep 17 00:00:00 2001 From: Sullivan SENECHAL Date: Sat, 11 Oct 2014 13:10:41 +0200 Subject: [PATCH 127/134] Add timezone configuration to gitlab.yml --- CHANGELOG | 3 +++ config/application.rb | 1 + config/gitlab.yml.example | 5 +++++ config/initializers/1_settings.rb | 1 + config/initializers/time_zone.rb | 1 + 5 files changed, 11 insertions(+) create mode 100644 config/initializers/time_zone.rb diff --git a/CHANGELOG b/CHANGELOG index 0529069832..f82ef4b4c7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +v 7.5.0 + - Add time zone configuration on gitlab.yml (Sullivan Senechal) + v 7.4.0 - Refactored membership logic - Improve error reporting on users API (Julien Bianchi) diff --git a/config/application.rb b/config/application.rb index e36df913d0..85c83f74a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,6 +25,7 @@ module Gitlab # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # NOTE: Please prefer set time zone on config/gitlab.yml configuration file. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e7a8d08dc8..2ca6abac57 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -33,6 +33,11 @@ production: &base # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # user: git + ## Date & Time settings + # Uncomment and customize if you want to change the default time zone of GitLab application. + # To see all available zones, run `bundle exec rake time:zones:all` + # time_zone: 'UTC' + ## Email settings # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 88cbaefea7..4670791ddb 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -103,6 +103,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= false Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) diff --git a/config/initializers/time_zone.rb b/config/initializers/time_zone.rb new file mode 100644 index 0000000000..ee246e67d6 --- /dev/null +++ b/config/initializers/time_zone.rb @@ -0,0 +1 @@ +Time.zone = Gitlab.config.gitlab.time_zone || Time.zone From cccfede34cac854b4a6cbbe64d83647ee0c0af35 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 21 Oct 2014 11:33:26 +0200 Subject: [PATCH 128/134] Add test for allowed team name of slack. --- spec/models/slack_service_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/models/slack_service_spec.rb b/spec/models/slack_service_spec.rb index 95df38d940..526165e397 100644 --- a/spec/models/slack_service_spec.rb +++ b/spec/models/slack_service_spec.rb @@ -77,5 +77,25 @@ describe SlackService do WebMock.should have_requested(:post, api_url).once end end + + context 'with new webhook syntax with slack allowed team name' do + before do + @allowed_webhook = 'https://gitlab-hq-123.slack.com/services/hooks/incoming-webhook?token=cdIj4r4LfXUOySDUjp0tk3OI' + slack_service.stub( + project: project, + project_id: project.id, + service_hook: true, + webhook: @allowed_webhook + ) + + WebMock.stub_request(:post, @allowed_webhook) + end + + it "should call Slack API" do + slack_service.execute(sample_data) + + WebMock.should have_requested(:post, @allowed_webhook).once + end + end end end From ce61de68ba43bd59dcec607dddae49591459bf93 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 21 Oct 2014 11:38:41 +0200 Subject: [PATCH 129/134] Use allowed slack team name. --- app/models/project_services/slack_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 95f3ddcef4..837002ef3c 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -40,7 +40,8 @@ class SlackService < Service project_name: project_name )) - credentials = webhook.match(/(\w*).slack.com.*services\/(.*)/) + credentials = webhook.match(/([\w-]*).slack.com.*services\/(.*)/) + if credentials.present? subdomain = credentials[1] token = credentials[2].split("token=").last From ce056d80748da32e20c3bfab1bff9567a812bfe1 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Tue, 21 Oct 2014 12:36:09 +0200 Subject: [PATCH 130/134] Improve grack auth hooks comment. --- lib/gitlab/backend/grack_auth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index c2f3b851c0..df1461a45c 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -90,7 +90,7 @@ module Grack when *Gitlab::GitAccess::PUSH_COMMANDS if user # Skip user authorization on upload request. - # It will be serverd by update hook in repository + # It will be done by the pre-receive hook in the repository. true else false From e6631c87860c182ce9c838da6b4ad8d570061dfb Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 21 Oct 2014 13:21:58 +0200 Subject: [PATCH 131/134] Merge request for blog post on gitlab.com next time. --- doc/release/monthly.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c46a3ed9c9..a9253339e5 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -191,6 +191,7 @@ It is important to do this as soon as possible, so we can catch any errors befor - Ask Dmitriy to add screenshots to the WIP MR. - Decide with team who will be the MVP user. - Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. +- Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) - Assign to one reviewer who will fix spelling issues by editing the branch (can use the online editor) - After the reviewer is finished the whole team will be mentioned to give their suggestions via line comments From da21b9e7d045a1f9b044563b62f09992ac685065 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 21 Oct 2014 18:26:40 +0300 Subject: [PATCH 132/134] Fix rake gitlab:ldap:check Signed-off-by: Dmitriy Zaporozhets --- lib/gitlab/ldap/adapter.rb | 6 ++++- lib/tasks/gitlab/check.rake | 44 ++++++++++--------------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index c4d0a20d89..256cdb4c2f 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -22,7 +22,7 @@ module Gitlab Gitlab::LDAP::Config.new(provider) end - def users(field, value) + def users(field, value, limit = nil) if field.to_sym == :dn options = { base: value, @@ -45,6 +45,10 @@ module Gitlab end end + if limit.present? + options.merge!(size: limit) + end + entries = ldap_search(options).select do |entry| entry.respond_to? config.uid end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 9ec368254a..707d236068 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -664,7 +664,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "LDAP" - if ldap_config.enabled + if Gitlab::LDAP::Config.enabled? print_users(args.limit) else puts 'LDAP is disabled in config/gitlab.yml' @@ -675,39 +675,19 @@ namespace :gitlab do def print_users(limit) puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - ldap.search(attributes: attributes, filter: filter, size: limit, return_result: false) do |entry| - puts "DN: #{entry.dn}\t#{ldap_config.uid}: #{entry[ldap_config.uid]}" + + servers = Gitlab.config.ldap.servers.keys + + servers.each do |server| + puts "Server: #{server}" + Gitlab::LDAP::Adapter.open("ldap#{server}") do |adapter| + users = adapter.users(adapter.config.uid, '*', 100) + users.each do |user| + puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + end + end end end - - def attributes - [ldap_config.uid] - end - - def filter - uid_filter = Net::LDAP::Filter.present?(ldap_config.uid) - if user_filter - Net::LDAP::Filter.join(uid_filter, user_filter) - else - uid_filter - end - end - - def user_filter - if ldap_config['user_filter'] && ldap_config.user_filter.present? - Net::LDAP::Filter.construct(ldap_config.user_filter) - else - nil - end - end - - def ldap - @ldap ||= OmniAuth::LDAP::Adaptor.new(ldap_config).connection - end - - def ldap_config - @ldap_config ||= Gitlab.config.ldap - end end # Helper methods From 05f19392b76d9fbe40d97547ee2a3c87883c9639 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 22 Oct 2014 11:11:18 +0300 Subject: [PATCH 133/134] Make gitlab ldap check work for old and new syntax Signed-off-by: Dmitriy Zaporozhets --- lib/tasks/gitlab/check.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 707d236068..56e8ff4498 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -676,11 +676,11 @@ namespace :gitlab do def print_users(limit) puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - servers = Gitlab.config.ldap.servers.keys + servers = Gitlab::LDAP::Config.providers servers.each do |server| puts "Server: #{server}" - Gitlab::LDAP::Adapter.open("ldap#{server}") do |adapter| + Gitlab::LDAP::Adapter.open(server) do |adapter| users = adapter.users(adapter.config.uid, '*', 100) users.each do |user| puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" From b0ef23c1936577b0d3a9d5e58c808bb24b41b0ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 22 Oct 2014 13:38:47 +0300 Subject: [PATCH 134/134] Fix 500 error on login page if ldap enabled and sign-in disabled Signed-off-by: Dmitriy Zaporozhets --- app/views/devise/sessions/new.html.haml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index b983278744..ca7e9570b4 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -2,22 +2,22 @@ .login-heading %h3 Sign in .login-body - - if ldap_enabled? && gitlab_config.signin_enabled + - if ldap_enabled? %ul.nav.nav-tabs - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i==0)} + %li{class: (:active if i.zero?)} = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + - if gitlab_config.signin_enabled + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' .tab-content - - @ldap_servers.each_with_index do |server,i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i==0)} + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)} = render 'devise/sessions/new_ldap', provider: server['provider_name'] - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' + - if gitlab_config.signin_enabled + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' - - elsif ldap_enabled? - = render 'devise/sessions/new_ldap', ldap_servers: @ldap_servers - elsif gitlab_config.signin_enabled = render 'devise/sessions/new_base' - else