From 454d227b45a563cb21ea8fa5091e687a2876380f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 11 Aug 2015 01:44:40 -0400 Subject: [PATCH 001/443] Remove all permission checking from Reference filters --- .../markdown/cross_project_reference.rb | 11 +--- lib/gitlab/markdown/user_reference_filter.rb | 6 -- .../commit_range_reference_filter_spec.rb | 54 ++++++---------- .../markdown/commit_reference_filter_spec.rb | 48 +++++--------- .../markdown/cross_project_reference_spec.rb | 12 ---- .../markdown/issue_reference_filter_spec.rb | 62 +++++++------------ .../merge_request_reference_filter_spec.rb | 48 +++++--------- .../markdown/snippet_reference_filter_spec.rb | 46 +++++--------- .../markdown/user_reference_filter_spec.rb | 48 +++++--------- spec/support/filter_spec_helper.rb | 14 ----- 10 files changed, 113 insertions(+), 236 deletions(-) diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb index 855748fdcc..6ab04a584b 100644 --- a/lib/gitlab/markdown/cross_project_reference.rb +++ b/lib/gitlab/markdown/cross_project_reference.rb @@ -13,18 +13,11 @@ module Gitlab # # ref - String reference. # - # Returns a Project, or nil if the reference can't be accessed + # Returns a Project, or nil if the reference can't be found def project_from_ref(ref) return context[:project] unless ref - other = Project.find_with_namespace(ref) - return nil unless other && user_can_reference_project?(other) - - other - end - - def user_can_reference_project?(project, user = context[:current_user]) - Ability.abilities.allowed?(user, :read_project, project) + Project.find_with_namespace(ref) end end end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 1871e52df0..0446cd49f8 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -80,8 +80,6 @@ module Gitlab end def link_to_group(group, namespace) - return unless user_can_reference_group?(namespace) - push_result(:user, *namespace.users) url = urls.group_url(group, only_path: context[:only_path]) @@ -100,10 +98,6 @@ module Gitlab text = User.reference_prefix + user %(#{text}) end - - def user_can_reference_group?(group) - Ability.abilities.allowed?(context[:current_user], :read_group, group) - end end end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 3c6c84a041..6813d6db14 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -106,45 +106,31 @@ module Gitlab::Markdown range.project = project2 end - context 'when user can access reference' do - before { allow_cross_reference! } + it 'links to a valid reference' do + doc = filter("See #{reference}") - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) - end - - it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") - - exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") - expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp - - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") - it 'ignores valid references' do - exp = act = "See #{reference}" + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) + end - expect(filter(act).to_html).to eq exp - end + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty end end end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index 9ed438252b..f937b4f50e 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -99,42 +99,28 @@ module Gitlab::Markdown let(:commit) { project2.commit } let(:reference) { commit.to_reference(project) } - context 'when user can access reference' do - before { allow_cross_reference! } + it 'links to a valid reference' do + doc = filter("See #{reference}") - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) - end - - it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") - - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(#{exp}@#{commit.short_id}<\/a>\.\)/) - end - - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") - it 'ignores valid references' do - exp = act = "See #{reference}" + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(#{exp}@#{commit.short_id}<\/a>\.\)/) + end - expect(filter(act).to_html).to eq exp - end + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty end end end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb index 4698d6138c..6490d6f7a4 100644 --- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -35,21 +35,9 @@ module Gitlab::Markdown context 'and the user has permission to read it' do it 'returns the referenced project' do - expect(self).to receive(:user_can_reference_project?). - with(project2).and_return(true) - expect(project_from_ref('cross/reference')).to eq project2 end end - - context 'and the user does not have permission to read it' do - it 'returns nil' do - expect(self).to receive(:user_can_reference_project?). - with(project2).and_return(false) - - expect(project_from_ref('cross/reference')).to be_nil - end - end end end end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 1dd54f5858..9678795451 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -96,49 +96,35 @@ module Gitlab::Markdown let(:issue) { create(:issue, project: project2) } let(:reference) { issue.to_reference(project) } - context 'when user can access reference' do - before { allow_cross_reference! } + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:get_issue). + with(issue.iid).and_return(nil) - it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(Project).to receive(:get_issue). - with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp - end - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) - end - - it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'links to a valid reference' do + doc = filter("See #{reference}") - it 'ignores valid references' do - exp = act = "See #{reference}" + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end - expect(filter(act).to_html).to eq exp - end + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 66616b9336..feba08f720 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -84,42 +84,28 @@ module Gitlab::Markdown let(:merge) { create(:merge_request, source_project: project2) } let(:reference) { merge.to_reference(project) } - context 'when user can access reference' do - before { allow_cross_reference! } + it 'links to a valid reference' do + doc = filter("See #{reference}") - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) - end - - it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid merge IDs on the referenced project' do - exp = act = "Merge #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project, merge) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end - it 'ignores valid references' do - exp = act = "See #{reference}" + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp - end + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index fd3f0d20fa..02d581a7c4 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -83,41 +83,27 @@ module Gitlab::Markdown let(:snippet) { create(:project_snippet, project: project2) } let(:reference) { snippet.to_reference(project) } - context 'when user can access reference' do - before { allow_cross_reference! } + it 'links to a valid reference' do + doc = filter("See #{reference}") - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) - end - - it 'links with adjacent text' do - doc = filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) - end - - it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{invalidate_reference(reference)}" - - expect(filter(act).to_html).to eq exp - end - - it 'adds to the results hash' do - result = pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(reference)}<\/a>\.\)/) + end - it 'ignores valid references' do - exp = act = "See #{reference}" + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp - end + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] end end end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index b2155fab59..d3028cd3b8 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -83,40 +83,26 @@ module Gitlab::Markdown let(:user) { create(:user) } let(:reference) { group.to_reference } - context 'that the current user can read' do - before do - group.add_developer(user) - end - - it 'links to the Group' do - doc = filter("Hey #{reference}", current_user: user) - expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) - end - - it 'includes a data-group-id attribute' do - doc = filter("Hey #{reference}", current_user: user) - link = doc.css('a').first - - expect(link).to have_attribute('data-group-id') - expect(link.attr('data-group-id')).to eq group.id.to_s - end - - it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}", current_user: user) - expect(result[:references][:user]).to eq group.users - end + before do + group.add_developer(user) end - context 'that the current user cannot read' do - it 'ignores references to the Group' do - doc = filter("Hey #{reference}", current_user: user) - expect(doc.to_html).to eq "Hey #{reference}" - end + it 'links to the Group' do + doc = filter("Hey #{reference}", current_user: user) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end - it 'does not add to the results hash' do - result = pipeline_result("Hey #{reference}", current_user: user) - expect(result[:references][:user]).to eq [] - end + it 'includes a data-group-id attribute' do + doc = filter("Hey #{reference}", current_user: user) + link = doc.css('a').first + + expect(link).to have_attribute('data-group-id') + expect(link.attr('data-group-id')).to eq group.id.to_s + end + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}", current_user: user) + expect(result[:references][:user]).to eq group.users end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 755964e9a3..25df389629 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -55,20 +55,6 @@ module FilterSpecHelper end end - # Stub CrossProjectReference#user_can_reference_project? to return true for - # the current test - def allow_cross_reference! - allow_any_instance_of(described_class). - to receive(:user_can_reference_project?).and_return(true) - end - - # Stub CrossProjectReference#user_can_reference_project? to return false for - # the current test - def disallow_cross_reference! - allow_any_instance_of(described_class). - to receive(:user_can_reference_project?).and_return(false) - end - # Shortcut to Rails' auto-generated routes helpers, to avoid including the # module def urls From 7f75300573ff8f8e610bf4b0a3b4f2832552a1e9 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 4 Aug 2015 22:25:08 -0400 Subject: [PATCH 002/443] Add RedactorFilter --- lib/gitlab/markdown.rb | 1 + lib/gitlab/markdown/redactor_filter.rb | 66 ++++++++++++++++ .../gitlab/markdown/redactor_filter_spec.rb | 76 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 lib/gitlab/markdown/redactor_filter.rb create mode 100644 spec/lib/gitlab/markdown/redactor_filter_spec.rb diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 097caf67a6..e07fdb702f 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -41,6 +41,7 @@ module Gitlab autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' + autoload :RedactorFilter, 'gitlab/markdown/redactor_filter' autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb new file mode 100644 index 0000000000..9faee4567a --- /dev/null +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -0,0 +1,66 @@ +require 'gitlab/markdown' +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # HTML filter that removes references to records that the current user does + # not have permission to view. + # + # Expected to be run in its own post-processing pipeline. + # + class RedactorFilter < HTML::Pipeline::Filter + def call + doc.css('a.gfm').each do |node| + unless user_can_reference?(node) + node.replace(node.text) + end + end + + doc + end + + def user_can_reference?(node) + if node.has_attribute?('data-group-id') + user_can_reference_group?(node.attr('data-group-id')) + elsif node.has_attribute?('data-project-id') + user_can_reference_project?(node.attr('data-project-id')) + elsif node.has_attribute?('data-user-id') + user_can_reference_user?(node.attr('data-user-id')) + else + false + end + end + + def user_can_reference_group?(id) + group = Group.find(id) + + group && can?(:read_group, group) + end + + def user_can_reference_project?(id) + project = Project.find(id) + + project && can?(:read_project, project) + end + + def user_can_reference_user?(id) + # Permit all user reference links + true + end + + private + + def abilities + Ability.abilities + end + + def can?(ability, object) + abilities.allowed?(current_user, ability, object) + end + + def current_user + context[:current_user] + end + end + end +end diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb new file mode 100644 index 0000000000..a6cf3c6423 --- /dev/null +++ b/spec/lib/gitlab/markdown/redactor_filter_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe RedactorFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + it 'ignores non-GFM links' do + html = %(See Google) + doc = filter(html, current_user: double) + + expect(doc.css('a').length).to eq 1 + end + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context 'with data-group-id' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group_id: group.id) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group_id: group.id) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + end + + context 'with data-project-id' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + + link = reference_link(project_id: project.id) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + project.team << [user, :master] + + link = reference_link(project_id: project.id) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + end + + context 'with data-user-id' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user_id: user.id) + doc = filter(link) + + expect(doc.css('a').length).to eq 1 + end + end + end +end From 1ec68ea9c208317b6d981da4bf7d022ccd62bb3e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 17:43:35 -0400 Subject: [PATCH 003/443] Default `user_can_reference?` to true when no attributes match Now, a reference link with a `.gfm` class but without one of our `data-*-id` attributes should be shown to the user rather than hidden. --- lib/gitlab/markdown/redactor_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index 9faee4567a..92925f866a 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -27,7 +27,7 @@ module Gitlab elsif node.has_attribute?('data-user-id') user_can_reference_user?(node.attr('data-user-id')) else - false + true end end From 8db4628b8a736e85b2f40c007f07b43f780065a6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 17:45:25 -0400 Subject: [PATCH 004/443] Rescue from `RecordNotFound` in RedactorFilter --- lib/gitlab/markdown/redactor_filter.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index 92925f866a..ae1c3c365b 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -35,12 +35,16 @@ module Gitlab group = Group.find(id) group && can?(:read_group, group) + rescue ActiveRecord::RecordNotFound + false end def user_can_reference_project?(id) project = Project.find(id) project && can?(:read_project, project) + rescue ActiveRecord::RecordNotFound + false end def user_can_reference_user?(id) From 81fc63bec042d4b8eb7ed3522393204f3b9a7a53 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 17:46:36 -0400 Subject: [PATCH 005/443] Remove `current_user` context from markdown and gfm helpers These helpers are no longer dependent on the current user state. Hooray! --- app/helpers/gitlab_markdown_helper.rb | 2 -- lib/gitlab/markdown.rb | 1 - 2 files changed, 3 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1ebfd92f11..9890ec7c75 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -46,7 +46,6 @@ module GitlabMarkdownHelper def markdown(text, context = {}) context.merge!( - current_user: current_user, path: @path, project: @project, project_wiki: @project_wiki, @@ -60,7 +59,6 @@ module GitlabMarkdownHelper # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) options.merge!( - current_user: current_user, path: @path, project: @project, project_wiki: @project_wiki, diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index e07fdb702f..478851fc65 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -85,7 +85,6 @@ module Gitlab no_header_anchors: options[:no_header_anchors], # ReferenceFilter - current_user: options[:current_user], only_path: options[:reference_only_path], project: options[:project], From 86f706b169bc77b7cf4323dffc5cc7124413eac1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 18:09:20 -0400 Subject: [PATCH 006/443] Fix GitlabMarkdownHelper spec --- spec/helpers/gitlab_markdown_helper_spec.rb | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 5639b3db91..0c2b300309 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:snippet) { create(:project_snippet, project: project) } - # Helper expects a current_user method. - let(:current_user) { user } - before do + # Ensure the generated reference links aren't redacted + project.team << [user, :master] + # Helper expects a @project instance variable - @project = project + helper.instance_variable_set(:@project, project) + + # Stub the `current_user` helper + allow(helper).to receive(:current_user).and_return(user) end describe "#markdown" do @@ -25,17 +28,17 @@ describe GitlabMarkdownHelper do it "should link to the merge request" do expected = namespace_project_merge_request_path(project.namespace, project, merge_request) - expect(markdown(actual)).to match(expected) + expect(helper.markdown(actual)).to match(expected) end it "should link to the commit" do expected = namespace_project_commit_path(project.namespace, project, commit) - expect(markdown(actual)).to match(expected) + expect(helper.markdown(actual)).to match(expected) end it "should link to the issue" do expected = namespace_project_issue_path(project.namespace, project, issue) - expect(markdown(actual)).to match(expected) + expect(helper.markdown(actual)).to match(expected) end end end @@ -45,7 +48,7 @@ describe GitlabMarkdownHelper do let(:issues) { create_list(:issue, 2, project: project) } it 'should handle references nested in links with all the text' do - actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) + actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) doc = Nokogiri::HTML.parse(actual) # Make sure we didn't create invalid markup @@ -75,7 +78,7 @@ describe GitlabMarkdownHelper do end it 'should forward HTML options' do - actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') + actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') doc = Nokogiri::HTML.parse(actual) expect(doc.css('a')).to satisfy do |v| @@ -86,13 +89,13 @@ describe GitlabMarkdownHelper do it "escapes HTML passed in as the body" do actual = "This is a

test

- see #{issues[0].to_reference}" - expect(link_to_gfm(actual, commit_path)). + expect(helper.link_to_gfm(actual, commit_path)). to match('<h1>test</h1>') end it 'ignores reference links when they are the entire body' do text = issues[0].to_reference - act = link_to_gfm(text, '/foo') + act = helper.link_to_gfm(text, '/foo') expect(act).to eq %Q(#{issues[0].to_reference}) end end From ca8d225307280750597150b0b969998f99aad4a5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 18:09:52 -0400 Subject: [PATCH 007/443] Update MarkdownFeature support class - Memoize variables a bit more cleanly - Add user to project's team --- spec/support/markdown_feature.rb | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 39a6439146..bedc1a7f1d 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -15,18 +15,17 @@ class MarkdownFeature end def group - unless @group - @group = create(:group) - @group.add_developer(user) + @group ||= create(:group).tap do |group| + group.add_developer(user) end - - @group end # Direct references ---------------------------------------------------------- def project - @project ||= create(:project) + @project ||= create(:project).tap do |project| + project.team << [user, :master] + end end def issue @@ -46,12 +45,10 @@ class MarkdownFeature end def commit_range - unless @commit_range + @commit_range ||= begin commit2 = project.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + CommitRange.new("#{commit.id}...#{commit2.id}", project) end - - @commit_range end def simple_label @@ -65,13 +62,12 @@ class MarkdownFeature # Cross-references ----------------------------------------------------------- def xproject - unless @xproject + @xproject ||= begin namespace = create(:namespace, name: 'cross-reference') - @xproject = create(:project, namespace: namespace) - @xproject.team << [user, :developer] + create(:project, namespace: namespace) do |project| + project.team << [user, :developer] + end end - - @xproject end def xissue @@ -91,12 +87,10 @@ class MarkdownFeature end def xcommit_range - unless @xcommit_range + @xcommit_range ||= begin xcommit2 = xproject.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) end - - @xcommit_range end def raw_markdown From 5794d65a0743343bfaa367d10d7b0aaa82e20a25 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 18:16:56 -0400 Subject: [PATCH 008/443] Add post_process method to Gitlab::Markdown --- app/helpers/gitlab_markdown_helper.rb | 6 ++++-- lib/gitlab/markdown.rb | 22 ++++++++++++++++++++++ spec/features/markdown_spec.rb | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 9890ec7c75..f2cab2840d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -52,7 +52,8 @@ module GitlabMarkdownHelper ref: @ref ) - Gitlab::Markdown.render(text, context) + html = Gitlab::Markdown.render(text, context) + Gitlab::Markdown.post_process(html, current_user) end # TODO (rspeicher): Remove all usages of this helper and just call `markdown` @@ -65,7 +66,8 @@ module GitlabMarkdownHelper ref: @ref ) - Gitlab::Markdown.gfm(text, options) + html = Gitlab::Markdown.gfm(text, options) + Gitlab::Markdown.post_process(html, current_user) end def asciidoc(text) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 478851fc65..dbb8da3f0a 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -31,6 +31,24 @@ module Gitlab renderer.render(markdown) end + # Perform post-processing on an HTML String + # + # This method is used to perform state-dependent changes to a String of + # HTML, such as removing references that the current user doesn't have + # permission to make (`RedactorFilter`). + # + # html - String to process + # for_user - User state + # + # Returns an HTML-safe String + def self.post_process(html, for_user) + result = post_processor.call(html, current_user: for_user) + + result[:output]. + to_html. + html_safe + end + # Provide autoload paths for filters to prevent a circular dependency error autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' @@ -115,6 +133,10 @@ module Gitlab end end + def self.post_processor + @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter]) + end + def self.redcarpet_options # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use @redcarpet_options ||= { diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index c557a1061a..fdd8cf07b1 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do end end - # `markdown` calls these two methods + # Fake a `current_user` helper def current_user @feat.user end From 21cacb36a728dd6e1d1bee77680a1c78645d1765 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 1 Sep 2015 18:32:52 -0400 Subject: [PATCH 009/443] Add RedactorFilter specs for invalid Group and Project references --- spec/lib/gitlab/markdown/redactor_filter_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb index a6cf3c6423..4ffba9ac7b 100644 --- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb +++ b/spec/lib/gitlab/markdown/redactor_filter_spec.rb @@ -37,6 +37,12 @@ module Gitlab::Markdown expect(doc.css('a').length).to eq 1 end + + it 'handles invalid Group references' do + link = reference_link(group_id: 12345) + + expect { filter(link) }.not_to raise_error + end end context 'with data-project-id' do @@ -60,6 +66,12 @@ module Gitlab::Markdown expect(doc.css('a').length).to eq 1 end + + it 'handles invalid Project references' do + link = reference_link(project_id: 12345) + + expect { filter(link) }.not_to raise_error + end end context 'with data-user-id' do From e5d89c1084d1c1f29bfabe50718debf8867f760e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 2 Sep 2015 23:45:42 -0400 Subject: [PATCH 010/443] Remove unnecessary current_user context from filter specs --- .../markdown/cross_project_reference_spec.rb | 24 +++++++------------ .../markdown/user_reference_filter_spec.rb | 11 +++------ 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb index 6490d6f7a4..8d4f9e403a 100644 --- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -2,20 +2,16 @@ require 'spec_helper' module Gitlab::Markdown describe CrossProjectReference do - # context in the html-pipeline sense, not in the rspec sense - let(:context) do - { - current_user: double('user'), - project: double('project') - } - end - include described_class describe '#project_from_ref' do context 'when no project was referenced' do it 'returns the project from context' do - expect(project_from_ref(nil)).to eq context[:project] + project = double + + allow(self).to receive(:context).and_return({ project: project }) + + expect(project_from_ref(nil)).to eq project end end @@ -26,17 +22,13 @@ module Gitlab::Markdown end context 'when referenced project exists' do - let(:project2) { double('referenced project') } + it 'returns the referenced project' do + project2 = double('referenced project') - before do expect(Project).to receive(:find_with_namespace). with('cross/reference').and_return(project2) - end - context 'and the user has permission to read it' do - it 'returns the referenced project' do - expect(project_from_ref('cross/reference')).to eq project2 - end + expect(project_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index d3028cd3b8..c206cf4b74 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -80,20 +80,15 @@ module Gitlab::Markdown context 'mentioning a group' do let(:group) { create(:group) } - let(:user) { create(:user) } let(:reference) { group.to_reference } - before do - group.add_developer(user) - end - it 'links to the Group' do - doc = filter("Hey #{reference}", current_user: user) + doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end it 'includes a data-group-id attribute' do - doc = filter("Hey #{reference}", current_user: user) + doc = filter("Hey #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-group-id') @@ -101,7 +96,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}", current_user: user) + result = pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq group.users end end From 4bd92e681e0a6d2a8d7e1ef44d9f248394833d09 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Sep 2015 16:38:35 -0400 Subject: [PATCH 011/443] Return early from markdown and gfm when text is empty --- app/helpers/gitlab_markdown_helper.rb | 4 ++++ app/views/events/_event_issue.atom.haml | 3 +-- app/views/events/_event_merge_request.atom.haml | 3 +-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index f2cab2840d..803578f191 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -45,6 +45,8 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) + return unless text.present? + context.merge!( path: @path, project: @project, @@ -59,6 +61,8 @@ module GitlabMarkdownHelper # TODO (rspeicher): Remove all usages of this helper and just call `markdown` # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) + return unless text.present? + options.merge!( path: @path, project: @project, diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 4259f64c19..4e8d70e4e9 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - - if issue.description.present? - = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) + = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index e8ed13df78..db2b3550c4 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - - if merge_request.description.present? - = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) + = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) From 3b690891f36975a35923f14388901f4f2a2c3ed9 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Sep 2015 17:35:50 -0400 Subject: [PATCH 012/443] Basic support for an Atom-specific rendering pipeline --- app/helpers/gitlab_markdown_helper.rb | 10 +- app/views/events/_event_issue.atom.haml | 2 +- .../events/_event_merge_request.atom.haml | 2 +- app/views/events/_event_note.atom.haml | 2 +- app/views/events/_event_push.atom.haml | 2 +- lib/gitlab/markdown.rb | 109 ++++++++++-------- 6 files changed, 69 insertions(+), 58 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 803578f191..0d175e1ea1 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -47,15 +47,16 @@ module GitlabMarkdownHelper def markdown(text, context = {}) return unless text.present? - context.merge!( + context.reverse_merge!( path: @path, + pipeline: :default, project: @project, project_wiki: @project_wiki, ref: @ref ) html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, current_user) + Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], user: current_user) end # TODO (rspeicher): Remove all usages of this helper and just call `markdown` @@ -63,15 +64,16 @@ module GitlabMarkdownHelper def gfm(text, options = {}) return unless text.present? - options.merge!( + options.reverse_merge!( path: @path, + pipeline: :default, project: @project, project_wiki: @project_wiki, ref: @ref ) html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, current_user) + Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], user: current_user) end def asciidoc(text) diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 4e8d70e4e9..fad6531002 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) + = markdown(issue.description, pipeline: :atom, project: issue.project) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index db2b3550c4..19bdc7b9ca 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) + = markdown(merge_request.description, pipeline: :atom, project: merge_request.project) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index cfbfba5020..b730ebbd5f 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project) + = markdown(note.note, pipeline: :atom, project: note.project) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 3625cb49d8..b271b9daff 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project) + %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project) - if event.commits_count > 15 %p %i diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index dbb8da3f0a..476c736a11 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -7,6 +7,14 @@ module Gitlab module Markdown # Convert a Markdown String into an HTML-safe String of HTML # + # Note that while the returned HTML will have been sanitized of dangerous + # HTML, it may post a risk of information leakage if it's not also passed + # through `post_process`. + # + # Also note that the returned String is always HTML, not XHTML. Views + # requiring XHTML, such as Atom feeds, need to call `post_process` on the + # result, providing the appropriate `pipeline` option. + # # markdown - Markdown String # context - Hash of context options passed to our HTML Pipeline # @@ -38,15 +46,19 @@ module Gitlab # permission to make (`RedactorFilter`). # # html - String to process - # for_user - User state + # options - Hash of options to customize output + # :pipeline - Symbol pipeline type + # :user - User object # # Returns an HTML-safe String - def self.post_process(html, for_user) - result = post_processor.call(html, current_user: for_user) + def self.post_process(html, options) + doc = post_processor.to_document(html, current_user: options[:user]) - result[:output]. - to_html. - html_safe + if options[:pipeline] == :atom + doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) + else + doc.to_html + end.html_safe end # Provide autoload paths for filters to prevent a circular dependency error @@ -68,26 +80,20 @@ module Gitlab autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - # Public: Parse the provided text with GitLab-Flavored Markdown + # Public: Parse the provided HTML with GitLab-Flavored Markdown # - # text - the source text - # options - A Hash of options used to customize output (default: {}): - # :xhtml - output XHTML instead of HTML - # :reference_only_path - Use relative path for reference links - def self.gfm(text, options = {}) - return text if text.nil? - - # Duplicate the string so we don't alter the original, then call to_str - # to cast it back to a String instead of a SafeBuffer. This is required - # for gsub calls to work as we need them to. - text = text.dup.to_str - - options.reverse_merge!( - xhtml: false, - reference_only_path: true, - project: options[:project], - current_user: options[:current_user] - ) + # html - HTML String + # options - A Hash of options used to customize output (default: {}) + # :no_header_anchors - Disable header anchors in TableOfContentsFilter + # :path - Current path String + # :pipeline - Symbol pipeline type + # :project - Current Project object + # :project_wiki - Current ProjectWiki object + # :ref - Current ref String + # + # Returns an HTML-safe String + def self.gfm(html, options = {}) + return '' unless html.present? @pipeline ||= HTML::Pipeline.new(filters) @@ -96,47 +102,39 @@ module Gitlab pipeline: options[:pipeline], # EmojiFilter - asset_root: Gitlab.config.gitlab.url, asset_host: Gitlab::Application.config.asset_host, - - # TableOfContentsFilter - no_header_anchors: options[:no_header_anchors], + asset_root: Gitlab.config.gitlab.url, # ReferenceFilter - only_path: options[:reference_only_path], - project: options[:project], + only_path: only_path_pipeline?(options[:pipeline]), + project: options[:project], # RelativeLinkFilter + project_wiki: options[:project_wiki], ref: options[:ref], requested_path: options[:path], - project_wiki: options[:project_wiki] + + # TableOfContentsFilter + no_header_anchors: options[:no_header_anchors] } - result = @pipeline.call(text, context) - - save_options = 0 - if options[:xhtml] - save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML - end - - text = result[:output].to_html(save_with: save_options) - - text.html_safe + @pipeline.to_html(html, context).html_safe end private - def self.renderer - @markdown ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) + # Check if a pipeline enables the `only_path` context option + # + # Returns Boolean + def self.only_path_pipeline?(pipeline) + case pipeline + when :atom, :email + false + else + true end end - def self.post_processor - @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter]) - end - def self.redcarpet_options # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use @redcarpet_options ||= { @@ -151,6 +149,17 @@ module Gitlab }.freeze end + def self.renderer + @markdown ||= begin + renderer = Redcarpet::Render::HTML.new + Redcarpet::Markdown.new(renderer, redcarpet_options) + end + end + + def self.post_processor + @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter]) + end + # Filters used in our pipeline # # SanitizationFilter should come first so that all generated reference HTML From 48837850838c8acb0c02ae60b29e18d287865878 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Sep 2015 17:47:27 -0400 Subject: [PATCH 013/443] Remove last remaining `reference_only_path` usages --- app/views/notify/_note_message.html.haml | 2 +- app/views/notify/new_issue_email.html.haml | 2 +- app/views/notify/new_merge_request_email.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 3fd4b04ac8..00cb4aa24c 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = markdown(@note.note, reference_only_path: false) + = markdown(@note.note, pipeline: :email) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 53a068be52..d3b799fca2 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = markdown(@issue.description, reference_only_path: false) + = markdown(@issue.description, pipeline: :email) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 5b7dd117c1..90ebdfc3fe 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description, reference_only_path: false) + = markdown(@merge_request.description, pipeline: :email) From c5280434399ee489eebda254b2d246252df68f2b Mon Sep 17 00:00:00 2001 From: Cristian Bica Date: Thu, 1 Oct 2015 17:05:20 +0300 Subject: [PATCH 014/443] Allow users to select the Files view as default project view --- app/assets/stylesheets/pages/projects.scss | 84 ++++++++++---------- app/controllers/projects_controller.rb | 5 +- app/helpers/preferences_helper.rb | 8 +- app/models/user.rb | 2 +- app/views/projects/_files.html.haml | 11 +++ app/views/projects/show.html.haml | 9 +-- app/views/projects/tree/_blob_item.html.haml | 2 +- app/views/projects/tree/_tree_item.html.haml | 2 +- spec/controllers/projects_controller_spec.rb | 25 ++++++ 9 files changed, 96 insertions(+), 52 deletions(-) create mode 100644 app/views/projects/_files.html.haml diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3105178567..7396de88cf 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1,6 +1,6 @@ .alert_holder { margin: -16px; - + .alert-link { font-weight: normal; } @@ -31,20 +31,20 @@ margin: -$gl-padding; padding: $gl-padding; padding: 44px 0 17px 0; - + .project-identicon-holder { margin-bottom: 16px; - + .avatar, .identicon { margin: 0 auto; float: none; } - + .identicon { @include border-radius(50%); } } - + .project-home-dropdown { margin: 11px 3px 0; } @@ -86,15 +86,15 @@ top: 17px; margin-bottom: 44px; } - + .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; - + .btn { @include bnt-project; @include btn-info; - + .count { display: inline-block; } @@ -105,7 +105,7 @@ .split-one { display: inline-table; margin-right: 12px; - + a { margin: -1px !important; } @@ -129,10 +129,10 @@ &.git-protocols { padding: 0; border: none; - + .input-group-btn:last-child > .btn { @include border-radius-right(0); - + border-left: 1px solid #c6cacf; margin-left: -2px !important; } @@ -141,55 +141,55 @@ } .projects-search-form { - + .input-group .form-control { height: 42px; } } .input-group-btn { - .btn { + .btn { @include bnt-project; @include btn-middle; - + &:hover { outline: none; } - + &:focus { outline: none; } - + &:active { outline: none; } } - + .active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - + border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; } - + .btn-green { @include btn-green } - + } .split-repo-buttons { display: inline-table; margin: 0 12px 0 12px; - + .btn{ @include bnt-project; @include btn-info; } - + .dropdown-toggle { margin: -5px; - } + } } #notification-form { @@ -202,7 +202,7 @@ .open > .dropdown-new.btn { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - + border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; text-transform: uppercase; @@ -214,21 +214,21 @@ .dropdown-menu { @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include border-radius (0px); - + border: none; padding: 16px 0; font-size: 14px; font-weight: 100; - + li a { color: #5f697a; line-height: 30px; - + &:hover { - background-color: #3084bb !important; + background-color: #3084bb !important; } } - + .fa-fw { margin-right: 8px; } @@ -370,7 +370,7 @@ table.table.protected-branches-list tr.no-border { ul.nav-pills { display:inline-block; } - + .nav-pills li { display:inline; } @@ -378,12 +378,12 @@ table.table.protected-branches-list tr.no-border { .nav > li > a { @include btn-info; @include bnt-project; - + background-color: transparent; border: 1px solid #f7f8fa; margin-left: 12px; } - + li { display:inline; } @@ -418,27 +418,27 @@ pre.light-well { .git-empty { margin: 0 7px 0 7px; - + h5 { color: #5c5d5e; } - + .light-well { @include border-radius (2px); - + color: #5b6169; - font-size: 13px; - line-height: 1.6em; + font-size: 13px; + line-height: 1.6em; } } .prepend-top-20 { margin-top: 20px; - + .btn-remove { @include btn-middle; @include btn-remove; - + float: left !important; } } @@ -446,7 +446,7 @@ pre.light-well { /* * Projects list rendered on dashboard and user page */ - + .projects-list { @include basic-list; @@ -507,6 +507,10 @@ pre.light-well { } } +.project-show-files { + padding-top: 20px; +} + .inline-form { display: inline-block; } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 213c2a7173..7158d4b49a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -89,7 +89,10 @@ class ProjectsController < ApplicationController if current_user @membership = @project.project_member_by_id(current_user.id) end - + @ref = "master" + @id = "master" + @commit = @project.repository.commit(@ref) + @tree = @project.repository.tree(@commit.id) render :show end else diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 1b1f4162df..f888c4a829 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -27,7 +27,8 @@ module PreferencesHelper def project_view_choices [ ['Readme (default)', :readme], - ['Activity view', :activity] + ['Activity view', :activity], + ['Files view', :files] ] end @@ -43,4 +44,9 @@ module PreferencesHelper !current_user || current_user.project_view == 'readme' end + + def current_user_default_project_view + (current_user && current_user.project_view) || + 'readme' + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3879f3fd38..f7a1589f5d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -176,7 +176,7 @@ class User < ActiveRecord::Base # User's Project preference # Note: When adding an option, it MUST go on the end of the array. - enum project_view: [:readme, :activity] + enum project_view: [:readme, :activity, :files] alias_attribute :private_token, :authentication_token diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml new file mode 100644 index 0000000000..2a99708eb4 --- /dev/null +++ b/app/views/projects/_files.html.haml @@ -0,0 +1,11 @@ += render 'projects/last_push' + +.tree-ref-holder + = render 'shared/ref_switcher', destination: 'tree', path: @path + +- if can? current_user, :download_code, @project + .tree-download-holder + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true + +#tree-holder.tree-holder.clearfix + = render "projects/tree/tree", tree: @tree diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 6a5fc68980..54afb7de15 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -64,13 +64,8 @@ Archived project! Repository is read-only %section - - if prefer_readme? - .project-show-readme - = render 'projects/readme' - - else - .project-show-activity - = render 'projects/activity' - + %div{class: "project-show-#{current_user_default_project_view}"} + = render current_user_default_project_view - if current_user - access = user_max_access_in_project(current_user, @project) diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index 02ecbade21..2ddc5d504f 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -4,5 +4,5 @@ %span.str-truncated = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) %td.tree_time_ago.cgray - = render 'spinner' + = render 'projects/tree/spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index e87138bf98..cf65057e70 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -5,5 +5,5 @@ - path = flatten_tree(tree_item) = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) %td.tree_time_ago.cgray - = render 'spinner' + = render 'projects/tree/spinner' %td.hidden-xs.tree_commit diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 29233e9fae..1e018acf42 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -21,6 +21,31 @@ describe ProjectsController do expect(response.body).to include("content='#{content}'") end end + + context "rendering default project view" do + render_views + + it "shold render the activity view", focus: true do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:project_view).and_return('activity') + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(response).to render_template('_activity') + end + + it "shold render the readme view", focus: true do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:project_view).and_return('readme') + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(response).to render_template('_readme') + end + + it "shold render the files view", focus: true do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:project_view).and_return('files') + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(response).to render_template('_files') + end + end end describe "POST #toggle_star" do From 16f8ca566b8637dc8092a6b630c23a82a905b437 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 23:40:29 -0400 Subject: [PATCH 015/443] Add custom protocol whitelisting to SanitizationFilter Addresses internal https://dev.gitlab.org/gitlab/gitlabhq/issues/2613 --- lib/gitlab/markdown/sanitization_filter.rb | 19 ++++ .../markdown/sanitization_filter_spec.rb | 101 ++++++++++++++++-- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index e368de7d84..ffb9dc33b6 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -48,6 +48,12 @@ module Gitlab # Allow span elements whitelist[:elements].push('span') + # Allow any protocol in `a` elements... + whitelist[:protocols].delete('a') + + # ...but then remove links with the `javascript` protocol + whitelist[:transformers].push(remove_javascript_links) + # Remove `rel` attribute from `a` elements whitelist[:transformers].push(remove_rel) @@ -57,6 +63,19 @@ module Gitlab whitelist end + def remove_javascript_links + lambda do |env| + node = env[:node] + + return unless node.name == 'a' + return unless node.has_attribute?('href') + + if node['href'].start_with?('javascript', ':javascript') + node.remove_attribute('href') + end + end + end + def remove_rel lambda do |env| if env[:node_name] == 'a' diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb index e50c82d0b3..27cd00e805 100644 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -44,7 +44,7 @@ module Gitlab::Markdown instance = described_class.new('Foo') 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 4 + expect(instance.whitelist[:transformers].size).to eq 5 end it 'allows syntax highlighting' do @@ -77,19 +77,100 @@ module Gitlab::Markdown end it 'removes `rel` attribute from `a` elements' do - doc = filter(%q{Link}) + act = %q{Link} + exp = %q{Link} - expect(doc.css('a').size).to eq 1 - expect(doc.at_css('a')['href']).to eq '#' - expect(doc.at_css('a')['rel']).to be_nil + expect(filter(act).to_html).to eq exp end - it 'removes script-like `href` attribute from `a` elements' do - html = %q{Hi} - doc = filter(html) + # Adapted from the Sanitize test suite: http://git.io/vczrM + protocols = { + 'protocol-based JS injection: simple, no spaces' => { + input: 'foo', + output: 'foo' + }, - expect(doc.css('a').size).to eq 1 - expect(doc.at_css('a')['href']).to be_nil + 'protocol-based JS injection: simple, spaces before' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: simple, spaces after' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: simple, spaces before and after' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: preceding colon' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: UTF-8 encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long UTF-8 encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: hex encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: long hex encoding' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: hex encoding without semicolons' => { + input: 'foo', + output: 'foo' + }, + + 'protocol-based JS injection: null char' => { + input: "foo", + output: '' + }, + + 'protocol-based JS injection: spaces and entities' => { + input: 'foo', + output: 'foo' + }, + } + + protocols.each do |name, data| + it "handles #{name}" do + doc = filter(data[:input]) + + expect(doc.to_html).to eq data[:output] + end + end + + it 'allows non-standard anchor schemes' do + exp = %q{IRC} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows relative links' do + exp = %q{foo/bar.md} + act = filter(exp) + + expect(act.to_html).to eq exp end end From d3734fbd89c069d35856b440f12109af8a7ef9c9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 14:43:19 +0200 Subject: [PATCH 016/443] Use tar for intermediate backup storage During the backup we create an intermediate copy of two directories: builds and uploads. Instead of creating many small files with 'cp -r', we now use tar (and fast gzip) to create single intermediate files. This saves on disk IO and disk space while creating a backup. --- lib/backup/builds.rb | 31 ++----------------------------- lib/backup/files.rb | 39 +++++++++++++++++++++++++++++++++++++++ lib/backup/manager.rb | 4 ++-- lib/backup/uploads.rb | 30 ++---------------------------- 4 files changed, 45 insertions(+), 59 deletions(-) create mode 100644 lib/backup/files.rb diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb index 6f56f680bb..d269f8e260 100644 --- a/lib/backup/builds.rb +++ b/lib/backup/builds.rb @@ -1,34 +1,7 @@ module Backup - class Builds - attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir - + class Builds < Files def initialize - @app_builds_dir = Settings.gitlab_ci.builds_path - @backup_dir = Gitlab.config.backup.path - @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds') - end - - # Copy builds from builds directory to backup/builds - def dump - FileUtils.rm_rf(backup_builds_dir) - # Ensure the parent dir of backup_builds_dir exists - FileUtils.mkdir_p(Gitlab.config.backup.path) - # Fail if somebody raced to create backup_builds_dir before us - FileUtils.mkdir(backup_builds_dir, mode: 0700) - FileUtils.cp_r(app_builds_dir, backup_dir) - end - - def restore - backup_existing_builds_dir - - FileUtils.cp_r(backup_builds_dir, app_builds_dir) - end - - def backup_existing_builds_dir - timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}") - if File.exists?(app_builds_dir) - FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path)) - end + super(Settings.gitlab_ci.builds_path) end end end diff --git a/lib/backup/files.rb b/lib/backup/files.rb new file mode 100644 index 0000000000..d0a6e8f27b --- /dev/null +++ b/lib/backup/files.rb @@ -0,0 +1,39 @@ +require 'open3' + +module Backup + class Files + attr_reader :name, :app_files_dir, :backup_tarball, :backup_dir, :files_parent_dir + + def initialize(app_files_dir) + @app_files_dir = File.realpath(app_files_dir) + @name = File.basename(app_files_dir) + @files_parent_dir = File.realpath(File.join(@app_files_dir, '..')) + @backup_dir = Gitlab.config.backup.path + @backup_tarball = File.join(@backup_dir, name + '.tar.gz') + end + + # Copy files from public/files to backup/files + def dump + FileUtils.mkdir_p(Gitlab.config.backup.path) + run_pipeline!([%W(tar -C #{files_parent_dir} -cf - #{name}), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + end + + def restore + backup_existing_files_dir + + run_pipeline!([%W(gzip -cd), %W(tar -C #{files_parent_dir} -xf -)], in: backup_tarball) + end + + def backup_existing_files_dir + timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}") + if File.exists?(app_files_dir) + FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path)) + end + end + + def run_pipeline!(cmd_list, options={}) + status_list = Open3.pipeline(*cmd_list, options) + abort 'Backup failed' unless status_list.compact.all?(&:success?) + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 5c42f25f4a..f011fd03de 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -150,11 +150,11 @@ module Backup private def backup_contents - folders_to_backup + ["backup_information.yml"] + folders_to_backup + ["uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] end def folders_to_backup - folders = %w{repositories db uploads builds} + folders = %w{repositories db} if ENV["SKIP"] return folders.reject{ |folder| ENV["SKIP"].include?(folder) } diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 1f9626644e..7c0838cc8b 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -1,34 +1,8 @@ module Backup - class Uploads - attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir + class Uploads < Files def initialize - @app_uploads_dir = File.realpath(Rails.root.join('public', 'uploads')) - @backup_dir = Gitlab.config.backup.path - @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads') - end - - # Copy uploads from public/uploads to backup/uploads - def dump - FileUtils.rm_rf(backup_uploads_dir) - # Ensure the parent dir of backup_uploads_dir exists - FileUtils.mkdir_p(Gitlab.config.backup.path) - # Fail if somebody raced to create backup_uploads_dir before us - FileUtils.mkdir(backup_uploads_dir, mode: 0700) - FileUtils.cp_r(app_uploads_dir, backup_dir) - end - - def restore - backup_existing_uploads_dir - - FileUtils.cp_r(backup_uploads_dir, app_uploads_dir) - end - - def backup_existing_uploads_dir - timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}") - if File.exists?(app_uploads_dir) - FileUtils.mv(app_uploads_dir, File.expand_path(timestamped_uploads_path)) - end + super(Rails.root.join('public/uploads')) end end end From 90ddf140b9390647002771572d0375da0bb9dfa4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 15:06:15 +0200 Subject: [PATCH 017/443] Reduce disk IO during SQL backup By using light gzip compression we can save a lot of disk IO during the backup. --- lib/backup/database.rb | 55 +++++++++++++++++++----------------------- lib/backup/manager.rb | 4 +-- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 959ac4b786..4bdf6e1c62 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -2,26 +2,27 @@ require 'yaml' module Backup class Database - attr_reader :config, :db_dir + attr_reader :config, :db_file_name def initialize @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] - @db_dir = File.join(Gitlab.config.backup.path, 'db') + @db_file_name = File.join(Gitlab.config.backup.path, 'database.sql.gz') end def dump - FileUtils.rm_rf(@db_dir) - # Ensure the parent dir of @db_dir exists - FileUtils.mkdir_p(Gitlab.config.backup.path) - # Fail if somebody raced to create @db_dir before us - FileUtils.mkdir(@db_dir, mode: 0700) + FileUtils.rm_f(db_file_name) + compress_rd, compress_wr = IO.pipe + compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600]) + compress_rd.close - success = case config["adapter"] + FileUtils.mkdir_p(Gitlab.config.backup.path) + + dump_pid = case config["adapter"] when /^mysql/ then $progress.print "Dumping MySQL database #{config['database']} ... " # Workaround warnings from MySQL 5.6 about passwords on cmd line ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] - system('mysqldump', *mysql_args, config['database'], out: db_file_name) + spawn('mysqldump', *mysql_args, config['database'], out: compress_wr) when "postgresql" then $progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env @@ -30,48 +31,42 @@ module Backup pgsql_args << "-n" pgsql_args << Gitlab.config.backup.pg_schema end - system('pg_dump', *pgsql_args, config['database'], out: db_file_name) + spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr) end + compress_wr.close + + success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? } + report_success(success) abort 'Backup failed' unless success - - $progress.print 'Compressing database ... ' - success = system('gzip', db_file_name) - report_success(success) - abort 'Backup failed: compress error' unless success end def restore - $progress.print 'Decompressing database ... ' - success = system('gzip', '-d', db_file_name_gz) - report_success(success) - abort 'Restore failed: decompress error' unless success + decompress_rd, decompress_wr = IO.pipe + decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name) + decompress_wr.close - success = case config["adapter"] + restore_pid = case config["adapter"] when /^mysql/ then $progress.print "Restoring MySQL database #{config['database']} ... " # Workaround warnings from MySQL 5.6 about passwords on cmd line ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] - system('mysql', *mysql_args, config['database'], in: db_file_name) + spawn('mysql', *mysql_args, config['database'], in: decompress_rd) when "postgresql" then $progress.print "Restoring PostgreSQL database #{config['database']} ... " pg_env - system('psql', config['database'], '-f', db_file_name) + spawn('psql', config['database'], in: decompress_rd) end + decompress_rd.close + + success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? } + report_success(success) abort 'Restore failed' unless success end protected - def db_file_name - File.join(db_dir, 'database.sql') - end - - def db_file_name_gz - File.join(db_dir, 'database.sql.gz') - end - def mysql_args args = { 'host' => '--host', diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f011fd03de..53e79d4d1f 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -150,11 +150,11 @@ module Backup private def backup_contents - folders_to_backup + ["uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] + folders_to_backup + ["database.sql.gz", "uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] end def folders_to_backup - folders = %w{repositories db} + folders = %w{repositories} if ENV["SKIP"] return folders.reject{ |folder| ENV["SKIP"].include?(folder) } From 7b71727c562b6f6337a180ae136be94bf0f6ed31 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 15:10:13 +0200 Subject: [PATCH 018/443] Remove old "files" tarball explicitly --- lib/backup/files.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/backup/files.rb b/lib/backup/files.rb index d0a6e8f27b..1b08e3324d 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -15,6 +15,7 @@ module Backup # Copy files from public/files to backup/files def dump FileUtils.mkdir_p(Gitlab.config.backup.path) + FileUtils.rm_f(backup_tarball) run_pipeline!([%W(tar -C #{files_parent_dir} -cf - #{name}), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600]) end From e789644783fae55f1095ffcc38b32f810f549caa Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 15:21:15 +0200 Subject: [PATCH 019/443] Keep old path: db/database.sql.gz Documentation elsewhere refers to this internal path, let's keep it. --- lib/backup/database.rb | 3 ++- lib/backup/manager.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 4bdf6e1c62..fe0434361e 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -6,10 +6,11 @@ module Backup def initialize @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] - @db_file_name = File.join(Gitlab.config.backup.path, 'database.sql.gz') + @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') end def dump + FileUtils.mkdir_p(File.dirname(db_file_name)) FileUtils.rm_f(db_file_name) compress_rd, compress_wr = IO.pipe compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600]) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 53e79d4d1f..f011fd03de 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -150,11 +150,11 @@ module Backup private def backup_contents - folders_to_backup + ["database.sql.gz", "uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] + folders_to_backup + ["uploads.tar.gz", "builds.tar.gz", "backup_information.yml"] end def folders_to_backup - folders = %w{repositories} + folders = %w{repositories db} if ENV["SKIP"] return folders.reject{ |folder| ENV["SKIP"].include?(folder) } From 7d58489fd908b2263f02e8919b1bd0b3fae1201d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 15:22:08 +0200 Subject: [PATCH 020/443] Remove unused variable --- lib/backup/files.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 1b08e3324d..5a210a0e46 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -2,14 +2,13 @@ require 'open3' module Backup class Files - attr_reader :name, :app_files_dir, :backup_tarball, :backup_dir, :files_parent_dir + attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir def initialize(app_files_dir) @app_files_dir = File.realpath(app_files_dir) @name = File.basename(app_files_dir) @files_parent_dir = File.realpath(File.join(@app_files_dir, '..')) - @backup_dir = Gitlab.config.backup.path - @backup_tarball = File.join(@backup_dir, name + '.tar.gz') + @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz') end # Copy files from public/files to backup/files From 901f5445785a754227d8b77ca535947ab8cbbfca Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 15:38:21 +0200 Subject: [PATCH 021/443] Remove superfluous mkdir -p --- lib/backup/database.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/backup/database.rb b/lib/backup/database.rb index fe0434361e..67b2a64bd1 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -16,8 +16,6 @@ module Backup compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600]) compress_rd.close - FileUtils.mkdir_p(Gitlab.config.backup.path) - dump_pid = case config["adapter"] when /^mysql/ then $progress.print "Dumping MySQL database #{config['database']} ... " From 852526e07fdac8cc11bdd4dd47cab71b39ae192c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 16:10:14 +0200 Subject: [PATCH 022/443] Spec fixes for new backup contents --- spec/tasks/gitlab/backup_rake_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 3be7dd4e52..9a881455ea 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -112,14 +112,14 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads repositories builds} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') - expect(tar_contents).to match('uploads/') + expect(tar_contents).to match('uploads.tar.gz') expect(tar_contents).to match('repositories/') - expect(tar_contents).to match('builds/') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/) + expect(tar_contents).to match('builds.tar.gz') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz)\/$/) end it 'should delete temp directories' do @@ -160,12 +160,12 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads repositories builds} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz} ) expect(tar_contents).to match('db/') - expect(tar_contents).to match('uploads/') - expect(tar_contents).to match('builds/') + expect(tar_contents).to match('uploads.tar.gz') + expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).not_to match('repositories/') end From b847baf8c405935415fcd4ad4a620d577f6526f8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 6 Oct 2015 16:11:59 +0200 Subject: [PATCH 023/443] One more backup spec fix Stop the 'uploads' part from actually running. --- spec/tasks/gitlab/backup_rake_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 9a881455ea..386ac9c837 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -55,6 +55,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke) + expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end From 12626f029dc4ab345f8afa1fd42a3c04723ec55e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 15:17:43 +0200 Subject: [PATCH 024/443] Return strings where expected --- app/helpers/gitlab_markdown_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 40161c5a64..6264b7f82e 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -45,7 +45,7 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) - return unless text.present? + return "" unless text.present? context.reverse_merge!( path: @path, @@ -62,7 +62,7 @@ module GitlabMarkdownHelper # TODO (rspeicher): Remove all usages of this helper and just call `markdown` # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) - return unless text.present? + return "" unless text.present? options.reverse_merge!( path: @path, From f42cfa9e8757161414f88e50d23b1d618bffad1f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 17:00:48 +0200 Subject: [PATCH 025/443] Refactor reference gathering to use a dedicated filter --- app/helpers/gitlab_markdown_helper.rb | 11 ++- .../markdown/commit_range_reference_filter.rb | 16 +++- .../markdown/commit_reference_filter.rb | 24 +++-- .../external_issue_reference_filter.rb | 3 +- lib/gitlab/markdown/issue_reference_filter.rb | 11 ++- lib/gitlab/markdown/label_reference_filter.rb | 11 ++- .../merge_request_reference_filter.rb | 11 ++- lib/gitlab/markdown/redactor_filter.rb | 44 ++------- lib/gitlab/markdown/reference_filter.rb | 34 ++++--- .../markdown/reference_gatherer_filter.rb | 49 ++++++++++ .../markdown/snippet_reference_filter.rb | 11 ++- lib/gitlab/markdown/user_reference_filter.rb | 42 ++++++--- lib/gitlab/reference_extractor.rb | 2 +- spec/helpers/gitlab_markdown_helper_spec.rb | 2 +- .../commit_range_reference_filter_spec.rb | 22 +++-- .../markdown/commit_reference_filter_spec.rb | 22 +++-- .../markdown/issue_reference_filter_spec.rb | 22 +++-- .../markdown/label_reference_filter_spec.rb | 18 ++-- .../merge_request_reference_filter_spec.rb | 22 +++-- .../gitlab/markdown/redactor_filter_spec.rb | 81 +++++++++-------- .../reference_gatherer_filter_spec.rb | 89 +++++++++++++++++++ .../markdown/snippet_reference_filter_spec.rb | 22 +++-- .../markdown/user_reference_filter_spec.rb | 20 ++--- spec/lib/gitlab/reference_extractor_spec.rb | 4 +- spec/support/filter_spec_helper.rb | 9 +- 25 files changed, 417 insertions(+), 185 deletions(-) create mode 100644 lib/gitlab/markdown/reference_gatherer_filter.rb create mode 100644 spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 6264b7f82e..1b8bb46d25 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -19,7 +19,8 @@ module GitlabMarkdownHelper escape_once(body) end - gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user) + user = current_user if defined?(current_user) + gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -55,8 +56,10 @@ module GitlabMarkdownHelper ref: @ref ) + user = current_user if defined?(current_user) + html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], user: current_user) + Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], user: user) end # TODO (rspeicher): Remove all usages of this helper and just call `markdown` @@ -72,8 +75,10 @@ module GitlabMarkdownHelper ref: @ref ) + user = current_user if defined?(current_user) + html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], user: current_user) + Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], user: user) end def asciidoc(text) diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index bb496135d9..e070edae0a 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -26,6 +26,18 @@ module Gitlab end end + def self.referenced_by(node) + project = Project.find(node.attr("data-project")) rescue nil + return unless project + + id = node.attr("data-commit-range") + range = CommitRange.new(id, project) + + return unless range.valid_commits? + + { commit_range: range } + end + def initialize(*args) super @@ -53,13 +65,11 @@ module Gitlab range = CommitRange.new(id, project) if range.valid_commits? - push_result(:commit_range, range) - url = url_for_commit_range(project, range) title = range.reference_title klass = reference_class(:commit_range) - data = data_attribute(project.id) + data = data_attribute(project: project.id, commit_range: id) project_ref += '@' if project_ref diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index fcbb2e936a..8cdbeb1f9c 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -26,6 +26,18 @@ module Gitlab end end + def self.referenced_by(node) + project = Project.find(node.attr("data-project")) rescue nil + return unless project + + id = node.attr("data-commit") + commit = commit_from_ref(project, id) + + return unless commit + + { commit: commit } + end + def call replace_text_nodes_matching(Commit.reference_pattern) do |content| commit_link_filter(content) @@ -39,17 +51,15 @@ module Gitlab # Returns a String with commit references replaced with links. All links # have `gfm` and `gfm-commit` class names attached for styling. def commit_link_filter(text) - self.class.references_in(text) do |match, commit_ref, project_ref| + self.class.references_in(text) do |match, id, project_ref| project = self.project_from_ref(project_ref) - if commit = commit_from_ref(project, commit_ref) - push_result(:commit, commit) - + if commit = self.class.commit_from_ref(project, id) url = url_for_commit(project, commit) title = escape_once(commit.link_title) klass = reference_class(:commit) - data = data_attribute(project.id) + data = data_attribute(project: project.id, commit: id) project_ref += '@' if project_ref @@ -62,9 +72,9 @@ module Gitlab end end - def commit_from_ref(project, commit_ref) + def self.commit_from_ref(project, id) if project && project.valid_repo? - project.commit(commit_ref) + project.commit(id) end end diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb index f7c43e1ca8..8f86f13976 100644 --- a/lib/gitlab/markdown/external_issue_reference_filter.rb +++ b/lib/gitlab/markdown/external_issue_reference_filter.rb @@ -47,8 +47,9 @@ module Gitlab title = escape_once("Issue in #{project.external_issue_tracker.title}") klass = reference_class(:issue) + data = data_attribute(project: project.id) - %(#{match}) end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index 01320f8079..cd2a9b7568 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -27,6 +27,13 @@ module Gitlab end end + def self.referenced_by(node) + issue = Issue.find(node.attr("data-issue")) rescue nil + return unless issue + + { issue: issue } + end + def call replace_text_nodes_matching(Issue.reference_pattern) do |content| issue_link_filter(content) @@ -45,13 +52,11 @@ module Gitlab project = self.project_from_ref(project_ref) if project && issue = project.get_issue(id) - push_result(:issue, issue) - url = url_for_issue(id, project, only_path: context[:only_path]) title = escape_once("Issue: #{issue.title}") klass = reference_class(:issue) - data = data_attribute(project.id) + data = data_attribute(project: project.id, issue: issue.id) %(#{render_colored_label(label)}) diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index ecbd263d0e..440574e574 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -27,6 +27,13 @@ module Gitlab end end + def self.referenced_by(node) + merge_request = MergeRequest.find(node.attr("data-merge-request")) rescue nil + return unless merge_request + + { merge_request: merge_request } + end + def call replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| merge_request_link_filter(content) @@ -45,11 +52,9 @@ module Gitlab project = self.project_from_ref(project_ref) if project && merge_request = project.merge_requests.find_by(iid: id) - push_result(:merge_request, merge_request) - title = escape_once("Merge Request: #{merge_request.title}") klass = reference_class(:merge_request) - data = data_attribute(project.id) + data = data_attribute(project: project.id, merge_request: merge_request.id) url = url_for_merge_request(merge_request, project) diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index ae1c3c365b..07ea6207d2 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -19,49 +19,19 @@ module Gitlab doc end + private + def user_can_reference?(node) - if node.has_attribute?('data-group-id') - user_can_reference_group?(node.attr('data-group-id')) - elsif node.has_attribute?('data-project-id') - user_can_reference_project?(node.attr('data-project-id')) - elsif node.has_attribute?('data-user-id') - user_can_reference_user?(node.attr('data-user-id')) + if node.has_attribute?('data-reference-filter') + reference_type = node.attr('data-reference-filter') + reference_filter = reference_type.constantize + + reference_filter.user_can_reference?(current_user, node) else true end end - def user_can_reference_group?(id) - group = Group.find(id) - - group && can?(:read_group, group) - rescue ActiveRecord::RecordNotFound - false - end - - def user_can_reference_project?(id) - project = Project.find(id) - - project && can?(:read_project, project) - rescue ActiveRecord::RecordNotFound - false - end - - def user_can_reference_user?(id) - # Permit all user reference links - true - end - - private - - def abilities - Ability.abilities - end - - def can?(ability, object) - abilities.allowed?(current_user, ability, object) - end - def current_user context[:current_user] end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 9b293c957d..ea6136b330 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -15,10 +15,17 @@ module Gitlab # Results: # :references - A Hash of references that were found and replaced. class ReferenceFilter < HTML::Pipeline::Filter - def initialize(*args) - super + def self.user_can_reference?(user, node) + if node.has_attribute?('data-project') + project = Project.find(node.attr('data-project')) rescue nil + Ability.abilities.allowed?(user, :read_project, project) + else + true + end + end - result[:references] = Hash.new { |hash, type| hash[type] = [] } + def self.referenced_by(node) + nil end # Returns a data attribute String to attach to a reference link @@ -28,13 +35,14 @@ module Gitlab # # Examples: # - # data_attribute(1) # => "data-project-id=\"1\"" - # data_attribute(2, :user) # => "data-user-id=\"2\"" - # data_attribute(3, :group) # => "data-group-id=\"3\"" + # data_attribute(project: 1) # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\"" + # data_attribute(user: 2) # => "data-reference-filter=\"SomeReferenceFilter\" data-user=\"2\"" + # data_attribute(group: 3) # => "data-reference-filter=\"SomeReferenceFilter\" data-group=\"3\"" # # Returns a String - def data_attribute(id, type = :project) - %Q(data-#{type}-id="#{id}") + def data_attribute(attributes = {}) + attributes[:reference_filter] = self.class.name + attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") end def escape_once(html) @@ -59,16 +67,6 @@ module Gitlab context[:project] end - # Add a reference to the pipeline's result Hash - # - # type - Singular Symbol reference type (e.g., :issue, :user, etc.) - # values - One or more Objects to add - def push_result(type, *values) - return if values.empty? - - result[:references][type].push(*values) - end - def reference_class(type) "gfm gfm-#{type}" end diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb new file mode 100644 index 0000000000..d64671e955 --- /dev/null +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -0,0 +1,49 @@ +require 'gitlab/markdown' +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # HTML filter that removes references to records that the current user does + # not have permission to view. + # + # Expected to be run in its own post-processing pipeline. + # + class ReferenceGathererFilter < HTML::Pipeline::Filter + def initialize(*) + super + + result[:references] ||= Hash.new { |hash, type| hash[type] = [] } + end + + def call + doc.css('a.gfm').each do |node| + gather_references(node) + end + + doc + end + + private + + def gather_references(node) + return unless node.has_attribute?('data-reference-filter') + + reference_type = node.attr('data-reference-filter') + reference_filter = reference_type.constantize + + return unless reference_filter.user_can_reference?(current_user, node) + + references = reference_filter.referenced_by(node) + return unless references + + references.each do |type, values| + result[:references][type].push(*values) + end + end + + def current_user + context[:current_user] + end + end + end +end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index e2cf89cb1d..a7396e9652 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -27,6 +27,13 @@ module Gitlab end end + def self.referenced_by(node) + snippet = Snippet.find(node.attr("data-snippet")) rescue nil + return unless snippet + + { snippet: snippet } + end + def call replace_text_nodes_matching(Snippet.reference_pattern) do |content| snippet_link_filter(content) @@ -45,11 +52,9 @@ module Gitlab project = self.project_from_ref(project_ref) if project && snippet = project.snippets.find_by(id: id) - push_result(:snippet, snippet) - title = escape_once("Snippet: #{snippet.title}") klass = reference_class(:snippet) - data = data_attribute(project.id) + data = data_attribute(project: project.id, snippet: snippet.id) url = url_for_snippet(snippet, project) diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index c08811effb..0d2be7499b 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -23,6 +23,34 @@ module Gitlab end end + def self.referenced_by(node) + if node.has_attribute?('data-group') + group = Group.find(node.attr('data-group')) rescue nil + return unless group + + { user: group.users } + elsif node.has_attribute?('data-user') + user = User.find(node.attr('data-user')) rescue nil + return unless user + + { user: user } + elsif node.has_attribute?('data-project') + project = Project.find(node.attr('data-project')) rescue nil + return unless project + + { user: project.team.members.flatten } + end + end + + def self.user_can_reference?(user, node) + if node.has_attribute?('data-group') + group = Group.find(node.attr('data-group')) rescue nil + Ability.abilities.allowed?(user, :read_group, group) + else + super + end + end + def call replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) @@ -61,14 +89,12 @@ module Gitlab def link_to_all project = context[:project] - # FIXME (rspeicher): Law of Demeter - push_result(:user, *project.team.members.flatten) - url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) + data = data_attribute(project: project.id) text = User.reference_prefix + 'all' - %(#{text}) + %(#{text}) end def link_to_namespace(namespace) @@ -80,20 +106,16 @@ module Gitlab end def link_to_group(group, namespace) - push_result(:user, *namespace.users) - url = urls.group_url(group, only_path: context[:only_path]) - data = data_attribute(namespace.id, :group) + data = data_attribute(group: namespace.id) text = Group.reference_prefix + group %(#{text}) end def link_to_user(user, namespace) - push_result(:user, namespace.owner) - url = urls.user_url(user, only_path: context[:only_path]) - data = data_attribute(namespace.owner_id, :user) + data = data_attribute(user: namespace.owner_id) text = User.reference_prefix + user %(#{text}) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 0961bd8042..2e546ef0d5 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -50,7 +50,7 @@ module Gitlab ignore_blockquotes: true } - pipeline = HTML::Pipeline.new([filter], context) + pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) result = pipeline.call(@text) result[:references][filter_type] diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 5d471f2f6e..762ec25c4f 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -44,7 +44,7 @@ describe GitlabMarkdownHelper do describe "override default project" do let(:actual) { issue.to_reference } - let(:second_project) { create(:project) } + let(:second_project) { create(:project, :public) } let(:second_issue) { create(:issue, project: second_project) } it 'should link to the issue' do diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 6813d6db14..e5b8d723fe 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe CommitRangeReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:commit1) { project.commit } let(:commit2) { project.commit("HEAD~2") } @@ -75,12 +75,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("See #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-commit-range attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-commit-range') + expect(link.attr('data-commit-range')).to eq range.to_reference end it 'supports an :only_path option' do @@ -92,14 +100,14 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("See #{reference}") + result = reference_pipeline_result("See #{reference}") expect(result[:references][:commit_range]).not_to be_empty end end context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } let(:reference) { range.to_reference(project) } before do @@ -129,7 +137,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("See #{reference}") + result = reference_pipeline_result("See #{reference}") expect(result[:references][:commit_range]).not_to be_empty end end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index f937b4f50e..d080efbf3d 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe CommitReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:commit) { project.commit } it 'requires project context' do @@ -71,12 +71,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("See #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-commit attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-commit') + expect(link.attr('data-commit')).to eq commit.id end it 'supports an :only_path context' do @@ -88,14 +96,14 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("See #{reference}") + result = reference_pipeline_result("See #{reference}") expect(result[:references][:commit]).not_to be_empty end end context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } let(:reference) { commit.to_reference(project) } @@ -119,7 +127,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("See #{reference}") + result = reference_pipeline_result("See #{reference}") expect(result[:references][:commit]).not_to be_empty end end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 9678795451..94c80ae661 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -8,7 +8,7 @@ module Gitlab::Markdown IssuesHelper end - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } it 'requires project context' do @@ -68,12 +68,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Issue #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-issue attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-issue') + expect(link.attr('data-issue')).to eq issue.id.to_s end it 'supports an :only_path context' do @@ -85,14 +93,14 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Fixed #{reference}") + result = reference_pipeline_result("Fixed #{reference}") expect(result[:references][:issue]).to eq [issue] end end context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } let(:reference) { issue.to_reference(project) } @@ -123,7 +131,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Fixed #{reference}") + result = reference_pipeline_result("Fixed #{reference}") expect(result[:references][:issue]).to eq [issue] end end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index e32089de37..fc21b65a84 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -5,7 +5,7 @@ module Gitlab::Markdown describe LabelReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } @@ -25,12 +25,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Label #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s end it 'supports an :only_path context' do @@ -42,7 +50,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Label #{reference}") + result = reference_pipeline_result("Label #{reference}") expect(result[:references][:label]).to eq [label] end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index feba08f720..3ef6cdfff3 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe MergeRequestReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do @@ -56,12 +56,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Merge #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-merge-request attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-merge-request') + expect(link.attr('data-merge-request')).to eq merge.id.to_s end it 'supports an :only_path context' do @@ -73,14 +81,14 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Merge #{reference}") + result = reference_pipeline_result("Merge #{reference}") expect(result[:references][:merge_request]).to eq [merge] end end context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } let(:merge) { create(:merge_request, source_project: project2) } let(:reference) { merge.to_reference(project) } @@ -104,7 +112,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Merge #{reference}") + result = reference_pipeline_result("Merge #{reference}") expect(result[:references][:merge_request]).to eq [merge] end end diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb index 4ffba9ac7b..eea3f1cf37 100644 --- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb +++ b/spec/lib/gitlab/markdown/redactor_filter_spec.rb @@ -16,41 +16,12 @@ module Gitlab::Markdown link_to('text', '', class: 'gfm', data: data) end - context 'with data-group-id' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group_id: group.id) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 0 - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group_id: group.id) - doc = filter(link, current_user: user) - - expect(doc.css('a').length).to eq 1 - end - - it 'handles invalid Group references' do - link = reference_link(group_id: 12345) - - expect { filter(link) }.not_to raise_error - end - end - - context 'with data-project-id' do + context 'with data-project' do it 'removes unpermitted Project references' do user = create(:user) project = create(:empty_project) - link = reference_link(project_id: project.id) + link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 0 @@ -61,27 +32,59 @@ module Gitlab::Markdown project = create(:empty_project) project.team << [user, :master] - link = reference_link(project_id: project.id) + link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name) doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 end it 'handles invalid Project references' do - link = reference_link(project_id: 12345) + link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name) expect { filter(link) }.not_to raise_error end end - context 'with data-user-id' do - it 'allows any User reference' do - user = create(:user) + context "for user references" do - link = reference_link(user_id: user.id) - doc = filter(link) + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) - expect(doc.css('a').length).to eq 1 + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + + expect { filter(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + doc = filter(link) + + expect(doc.css('a').length).to eq 1 + end end end end diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb new file mode 100644 index 0000000000..4fa473ad19 --- /dev/null +++ b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe ReferenceGathererFilter do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + def reference_link(data) + link_to('text', '', class: 'gfm', data: data) + end + + context "for issue references" do + + context 'with data-project' do + it 'removes unpermitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to be_empty + end + + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + issue = create(:issue, project: project) + project.team << [user, :master] + + link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:issue]).to eq([issue]) + end + + it 'handles invalid Project references' do + link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name) + + expect { pipeline_result(link) }.not_to raise_error + end + end + end + + context "for user references" do + + context 'with data-group' do + it 'removes unpermitted Group references' do + user = create(:user) + group = create(:group) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to be_empty + end + + it 'allows permitted Group references' do + user = create(:user) + group = create(:group) + group.add_developer(user) + + link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link, current_user: user) + + expect(result[:references][:user]).to eq([user]) + end + + it 'handles invalid Group references' do + link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + + expect { pipeline_result(link) }.not_to raise_error + end + end + + context 'with data-user' do + it 'allows any User reference' do + user = create(:user) + + link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name) + result = pipeline_result(link) + + expect(result[:references][:user]).to eq([user]) + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 02d581a7c4..9d9652dba4 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe SnippetReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:snippet) { create(:project_snippet, project: project) } let(:reference) { snippet.to_reference } @@ -55,12 +55,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Snippet #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-snippet attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-snippet') + expect(link.attr('data-snippet')).to eq snippet.id.to_s end it 'supports an :only_path context' do @@ -72,14 +80,14 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Snippet #{reference}") + result = reference_pipeline_result("Snippet #{reference}") expect(result[:references][:snippet]).to eq [snippet] end end context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:snippet) { create(:project_snippet, project: project2) } let(:reference) { snippet.to_reference(project) } @@ -102,7 +110,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Snippet #{reference}") + result = reference_pipeline_result("Snippet #{reference}") expect(result[:references][:snippet]).to eq [snippet] end end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index c206cf4b74..d9e0d7c42d 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe UserReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:user) { create(:user) } let(:reference) { user.to_reference } @@ -39,7 +39,7 @@ module Gitlab::Markdown end it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}") + result = reference_pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq [project.creator] end end @@ -64,16 +64,16 @@ module Gitlab::Markdown expect(doc.css('a').length).to eq 1 end - it 'includes a data-user-id attribute' do + it 'includes a data-user attribute' do doc = filter("Hey #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-user-id') - expect(link.attr('data-user-id')).to eq user.namespace.owner_id.to_s + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}") + result = reference_pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq [user] end end @@ -87,16 +87,16 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end - it 'includes a data-group-id attribute' do + it 'includes a data-group attribute' do doc = filter("Hey #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-group-id') - expect(link.attr('data-group-id')).to eq group.id.to_s + expect(link).to have_attribute('data-group') + expect(link.attr('data-group')).to eq group.id.to_s end it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}") + result = reference_pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq group.users end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 088e34f050..6d7a067e4e 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do let(:project) { create(:project) } - subject { Gitlab::ReferenceExtractor.new(project, project.creator) } + subject { Gitlab::ReferenceExtractor.new(project, project.owner) } it 'accesses valid user objects' do @u_foo = create(:user, username: 'foo') @@ -102,7 +102,7 @@ describe Gitlab::ReferenceExtractor do let(:issue) { create(:issue, project: other_project) } before do - other_project.team << [project.creator, :developer] + other_project.team << [project.owner, :developer] end it 'handles project issue references' do diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 56ce88abb4..97e5c270a5 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -29,12 +29,19 @@ module FilterSpecHelper # # Returns the Hash def pipeline_result(body, contexts = {}) - contexts.reverse_merge!(project: project) + contexts.reverse_merge!(project: project) if defined?(project) pipeline = HTML::Pipeline.new([described_class], contexts) pipeline.call(body) end + def reference_pipeline_result(body, contexts = {}) + contexts.reverse_merge!(project: project) if defined?(project) + + pipeline = HTML::Pipeline.new([described_class, Gitlab::Markdown::ReferenceGathererFilter], contexts) + pipeline.call(body) + end + # Modify a String reference to make it invalid # # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get From 72afcbcd37f35e02544290977c2c91ae37c12c01 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 19:19:23 +0200 Subject: [PATCH 026/443] Always allow references to the current project --- app/models/concerns/mentionable.rb | 1 - lib/gitlab/markdown/redactor_filter.rb | 2 +- lib/gitlab/markdown/reference_filter.rb | 7 +++++-- lib/gitlab/markdown/reference_gatherer_filter.rb | 2 +- lib/gitlab/markdown/user_reference_filter.rb | 2 +- spec/lib/gitlab/reference_extractor_spec.rb | 4 ++-- spec/support/mentionable_shared_examples.rb | 2 ++ 7 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 5b0ae41164..7ad8d5b7da 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -61,7 +61,6 @@ module Mentionable ext = Gitlab::ReferenceExtractor.new(p, current_user) ext.analyze(text) - (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] end diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index 07ea6207d2..a1f3a8a8eb 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -26,7 +26,7 @@ module Gitlab reference_type = node.attr('data-reference-filter') reference_filter = reference_type.constantize - reference_filter.user_can_reference?(current_user, node) + reference_filter.user_can_reference?(current_user, node, context) else true end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index ea6136b330..8ad52479d3 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -15,9 +15,12 @@ module Gitlab # Results: # :references - A Hash of references that were found and replaced. class ReferenceFilter < HTML::Pipeline::Filter - def self.user_can_reference?(user, node) + def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') - project = Project.find(node.attr('data-project')) rescue nil + project_id = node.attr('data-project').to_i + return true if project_id == context[:project].id + + project = Project.find(project_id) rescue nil Ability.abilities.allowed?(user, :read_project, project) else true diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index d64671e955..171ff906da 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -31,7 +31,7 @@ module Gitlab reference_type = node.attr('data-reference-filter') reference_filter = reference_type.constantize - return unless reference_filter.user_can_reference?(current_user, node) + return unless reference_filter.user_can_reference?(current_user, node, context) references = reference_filter.referenced_by(node) return unless references diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 0d2be7499b..4567e98369 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -42,7 +42,7 @@ module Gitlab end end - def self.user_can_reference?(user, node) + def self.user_can_reference?(user, node, context) if node.has_attribute?('data-group') group = Group.find(node.attr('data-group')) rescue nil Ability.abilities.allowed?(user, :read_group, group) diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 6d7a067e4e..088e34f050 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do let(:project) { create(:project) } - subject { Gitlab::ReferenceExtractor.new(project, project.owner) } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } it 'accesses valid user objects' do @u_foo = create(:user, username: 'foo') @@ -102,7 +102,7 @@ describe Gitlab::ReferenceExtractor do let(:issue) { create(:issue, project: other_project) } before do - other_project.team << [project.owner, :developer] + other_project.team << [project.creator, :developer] end it 'handles project issue references' do diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index e3de0afb44..2b52b4c9b1 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -50,6 +50,8 @@ def common_mentionable_setup } extra_commits.each { |c| commitmap[c.short_id] = c } + allow(Project).to receive(:find).and_call_original + allow(Project).to receive(:find).with(project.id.to_s).and_return(project) allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } set_mentionable_text.call(ref_string) From e0f9d3a547e340d229f6164c917d19955a10b371 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 18:25:36 -0400 Subject: [PATCH 027/443] Update ReferenceFilter docs [ci skip] --- lib/gitlab/markdown/reference_filter.rb | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 8ad52479d3..d334a89150 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -11,15 +11,12 @@ module Gitlab # Context options: # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. - # - # Results: - # :references - A Hash of references that were found and replaced. class ReferenceFilter < HTML::Pipeline::Filter def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i return true if project_id == context[:project].id - + project = Project.find(project_id) rescue nil Ability.abilities.allowed?(user, :read_project, project) else @@ -33,14 +30,16 @@ module Gitlab # Returns a data attribute String to attach to a reference link # - # id - Object ID - # type - Object type (default: :project) + # attributes - Hash, where the key becomes the data attribute name and the + # value is the data attribute value # # Examples: # - # data_attribute(project: 1) # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\"" - # data_attribute(user: 2) # => "data-reference-filter=\"SomeReferenceFilter\" data-user=\"2\"" - # data_attribute(group: 3) # => "data-reference-filter=\"SomeReferenceFilter\" data-group=\"3\"" + # data_attribute(project: 1, issue: 2) + # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # + # data_attribute(project: 3, merge_request: 4) + # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) @@ -86,7 +85,7 @@ module Gitlab # Yields the current node's String contents. The result of the block will # replace the node's existing content and update the current document. # - # Returns the updated Nokogiri::XML::Document object. + # Returns the updated Nokogiri::HTML::DocumentFragment object. def replace_text_nodes_matching(pattern) return doc if project.nil? From e6419cecf4221d44be6e80a41ed668bd79f247fb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 8 Oct 2015 12:58:47 +0200 Subject: [PATCH 028/443] Update inline doc --- lib/gitlab/markdown/reference_gatherer_filter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index 171ff906da..89acb312cd 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -3,8 +3,8 @@ require 'html/pipeline/filter' module Gitlab module Markdown - # HTML filter that removes references to records that the current user does - # not have permission to view. + # HTML filter that gathers all referenced records that the current user has + # permission to view. # # Expected to be run in its own post-processing pipeline. # From 9d066bc9417b06c8eaa8a7e5e73b153093102bbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 8 Oct 2015 13:00:15 +0200 Subject: [PATCH 029/443] Raise error when a ReferenceFilter doesn't implement referenced_by --- lib/gitlab/markdown/reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index d334a89150..c7d4b15d45 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -25,7 +25,7 @@ module Gitlab end def self.referenced_by(node) - nil + raise NotImplementedError, "#{self} does not implement #{__method__}" end # Returns a data attribute String to attach to a reference link From cb13980db88c1d1ae8a5cd766ced4629c657010b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 8 Oct 2015 17:12:00 +0200 Subject: [PATCH 030/443] Let gitlab-git-http-server handle archive downloads This change relies on changes in gitlab_git and gitlab-git-http-server. --- .../projects/repositories_controller.rb | 17 ++----- app/services/archive_repository_service.rb | 45 ++----------------- lib/api/repositories.rb | 13 +----- lib/gitlab/backend/grack_auth.rb | 9 +++- lib/support/nginx/gitlab | 20 ++++++++- lib/support/nginx/gitlab-ssl | 20 ++++++++- 6 files changed, 54 insertions(+), 70 deletions(-) diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index c4a5e2d635..ba9aea1c16 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,18 +11,9 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - begin - file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute - rescue - return head :not_found - end - - if file_path - # Send file to user - response.headers["Content-Length"] = File.open(file_path).size.to_s - send_file file_path - else - redirect_to request.fullpath - end + render json: ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute + rescue => ex + logger.error("#{self.class.name}: #{ex}") + return git_not_found! end end diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb index e1b41527d8..6414b5a018 100644 --- a/app/services/archive_repository_service.rb +++ b/app/services/archive_repository_service.rb @@ -9,17 +9,10 @@ class ArchiveRepositoryService def execute(options = {}) project.repository.clean_old_archives - raise "No archive file path" unless file_path + metadata = project.repository.archive_metadata(ref, storage_path, format) + raise "Repository or ref not found" if metadata.empty? - return file_path if archived? - - unless archiving? - RepositoryArchiveWorker.perform_async(project.id, ref, format) - end - - archived = wait_until_archived(options[:timeout] || 5.0) - - file_path if archived + metadata end private @@ -27,36 +20,4 @@ class ArchiveRepositoryService def storage_path Gitlab.config.gitlab.repository_downloads_path end - - def file_path - @file_path ||= project.repository.archive_file_path(ref, storage_path, format) - end - - def pid_file_path - @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) - end - - def archived? - File.exist?(file_path) - end - - def archiving? - File.exist?(pid_file_path) - end - - def wait_until_archived(timeout = 5.0) - return archived? if timeout == 0.0 - - t1 = Time.now - - begin - sleep 0.1 - - success = archived? - - t2 = Time.now - end until success || t2 - t1 >= timeout - - success - end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 2d96c9666d..20d568cf46 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -133,7 +133,7 @@ module API authorize! :download_code, user_project begin - file_path = ArchiveRepositoryService.new( + ArchiveRepositoryService.new( user_project, params[:sha], params[:format] @@ -141,17 +141,6 @@ module API rescue not_found!('File') end - - if file_path && File.exists?(file_path) - data = File.open(file_path, 'rb').read - basename = File.basename(file_path) - header['Content-Disposition'] = "attachment; filename=\"#{basename}\"" - content_type MIME::Types.type_for(file_path).first.content_type - env['api.format'] = :binary - present data - else - redirect request.fullpath - end end # Compare two branches, tags or commits diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 0353b3b7ed..6830a916bc 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -193,7 +193,14 @@ module Grack end def render_grack_auth_ok - [200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user) })]] + [ + 200, + { "Content-Type" => "application/json" }, + [JSON.dump({ + 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), + 'RepoPath' => project.repository.path_to_repo, + })] + ] end def render_not_found diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 7218a4d2f2..ffc0eb0585 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -113,7 +113,25 @@ server { proxy_pass http://gitlab; } - location ~ [-\/\w\.]+\.git\/ { + location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location ~ ^/api/v3/projects/[0-9]+/repository/archive { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location @gitlab-git-http-server { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. # gzip off; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 7dabfba87e..c2e9f8864f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -160,7 +160,25 @@ server { proxy_pass http://gitlab; } - location ~ [-\/\w\.]+\.git\/ { + location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location ~ ^/api/v3/projects/[0-9]+/repository/archive { + # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block + error_page 418 = @gitlab-git-http-server; + return 418; + } + + location @gitlab-git-http-server { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. gzip off; From ae4fbae26cefbf10848719ee8c06d418c348420c Mon Sep 17 00:00:00 2001 From: Jonathan Rochkind Date: Thu, 8 Oct 2015 11:13:28 -0400 Subject: [PATCH 031/443] Send an email (to support) when a user is reported for spam --- app/controllers/abuse_reports_controller.rb | 3 ++ .../admin/application_settings_controller.rb | 1 + app/mailers/abuse_report_mailer.rb | 8 +++ .../abuse_report_mailer/notify.text.haml | 5 ++ .../application_settings/_form.html.haml | 4 ++ ...19_add_admin_notification_email_setting.rb | 5 ++ db/schema.rb | 3 +- .../abuse_reports_controller_spec.rb | 53 +++++++++++++++++++ 8 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app/mailers/abuse_report_mailer.rb create mode 100644 app/views/abuse_report_mailer/notify.text.haml create mode 100644 db/migrate/20151008143519_add_admin_notification_email_setting.rb create mode 100644 spec/controllers/abuse_reports_controller_spec.rb diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 65dbd5ef55..482ec5054a 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -11,6 +11,9 @@ class AbuseReportsController < ApplicationController if @abuse_report.save message = "Thank you for your report. A GitLab administrator will look into it shortly." redirect_to root_path, notice: message + if current_application_settings.admin_notification_email.present? + AbuseReportMailer.delay.notify(@abuse_report, current_application_settings.admin_notification_email) + end else render :new end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 5f70582cbb..18a258c139 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -55,6 +55,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :default_snippet_visibility, :restricted_signup_domains_raw, :version_check_enabled, + :admin_notification_email, :user_oauth_applications, :ci_enabled, restricted_visibility_levels: [], diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb new file mode 100644 index 0000000000..c8b9c9c162 --- /dev/null +++ b/app/mailers/abuse_report_mailer.rb @@ -0,0 +1,8 @@ +class AbuseReportMailer < BaseMailer + + def notify(abuse_report, to_email) + @abuse_report = abuse_report + + mail(to: to_email, subject: "[Gitlab] Abuse report filed for `#{@abuse_report.user.username}`") + end +end diff --git a/app/views/abuse_report_mailer/notify.text.haml b/app/views/abuse_report_mailer/notify.text.haml new file mode 100644 index 0000000000..70e4e6a3c6 --- /dev/null +++ b/app/views/abuse_report_mailer/notify.text.haml @@ -0,0 +1,5 @@ +An abuse report was filed on `#{@abuse_report.user.username}` by `#{@abuse_report.reporter.username}`. +\ += @abuse_report.message +\ +Abuse report admin screen: #{abuse_reports_url} \ No newline at end of file diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 143cd10c54..036e24d3a4 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -47,6 +47,10 @@ = f.label :version_check_enabled do = f.check_box :version_check_enabled Version check enabled + .form-group + = f.label :admin_notification_email, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' %fieldset %legend Account and Limit Settings diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb new file mode 100644 index 0000000000..0bb581efe2 --- /dev/null +++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb @@ -0,0 +1,5 @@ +class AddAdminNotificationEmailSetting < ActiveRecord::Migration + def change + add_column :application_settings, :admin_notification_email, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 72609da93f..23627bdaa2 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: 20150930095736) do +ActiveRecord::Schema.define(version: 20151008143519) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -47,6 +47,7 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.text "import_sources" t.text "help_page_text" t.boolean "ci_enabled", default: true, null: false + t.string "admin_notification_email" end create_table "audit_events", force: true do |t| diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb new file mode 100644 index 0000000000..6d157406a2 --- /dev/null +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe AbuseReportsController do + let(:reporter) { create(:user) } + let(:user) { create(:user) } + let(:message) { "This user is a spammer" } + + before do + sign_in(reporter) + end + + describe "with admin notification_email set" do + let(:admin_email) { "admin@example.com"} + before(:example) { allow(current_application_settings).to receive(:admin_notification_email).and_return(admin_email) } + + it "sends a notification email" do + post(:create, + abuse_report: { + user_id: user.id, + message: message + } + ) + + expect(response).to have_http_status(:redirect) + expect(flash[:notice]).to start_with("Thank you for your report") + + email = ActionMailer::Base.deliveries.last + + expect(email).to be_present + expect(email.subject).to eq("[Gitlab] Abuse report filed for `#{user.username}`") + expect(email.to).to eq([admin_email]) + expect(email.body).to include(message) + end + end + + describe "without admin notification email set" do + before(:example) { allow(current_application_settings).to receive(:admin_notification_email).and_return(nil) } + + it "does not send a notification email" do + expect do + post(:create, + abuse_report: { + user_id: user.id, + message: message + } + ) + end.to_not change{ActionMailer::Base.deliveries} + + expect(response).to have_http_status(:redirect) + expect(flash[:notice]).to start_with("Thank you for your report") + end + end +end \ No newline at end of file From f4cd4086815c6ec85d4714d037a09faa903f572b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 18:12:43 -0400 Subject: [PATCH 032/443] Remove guard-rspec and its supporting gems --- Gemfile | 16 ---------------- Gemfile.lock | 23 ----------------------- 2 files changed, 39 deletions(-) diff --git a/Gemfile b/Gemfile index 044dc30ecd..7ed5e11a1b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,13 +1,5 @@ source "https://rubygems.org" -def darwin_only(require_as) - RUBY_PLATFORM.include?('darwin') && require_as -end - -def linux_only(require_as) - RUBY_PLATFORM.include?('linux') && require_as -end - gem 'rails', '4.1.12' # Specify a sprockets version due to security issue @@ -308,11 +300,3 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" - -group :development, :test do - gem 'guard-rspec', '~> 4.2.0' - - gem 'rb-fsevent', require: darwin_only('rb-fsevent') - gem 'growl', require: darwin_only('growl') - gem 'rb-inotify', require: linux_only('rb-inotify') -end diff --git a/Gemfile.lock b/Gemfile.lock index f716c0254e..873583b4b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -315,19 +315,6 @@ GEM grape-entity (0.4.8) activesupport multi_json (>= 1.3.2) - growl (1.0.3) - guard (2.13.0) - formatador (>= 0.2.4) - listen (>= 2.7, <= 4.0) - lumberjack (~> 1.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.9.12) - shellany (~> 0.0) - thor (>= 0.18.1) - guard-rspec (4.2.10) - guard (~> 2.1) - rspec (>= 2.14, < 4.0) haml (4.0.7) tilt haml-rails (0.9.0) @@ -388,7 +375,6 @@ GEM celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - lumberjack (1.0.9) macaddr (1.7.1) systemu (~> 2.6.2) mail (2.6.3) @@ -404,7 +390,6 @@ GEM multi_xml (0.5.5) multipart-post (2.0.0) mysql2 (0.3.20) - nenv (0.2.0) nested_form (0.3.2) net-ldap (0.11) net-scp (1.2.1) @@ -417,9 +402,6 @@ GEM newrelic_rpm (3.9.4.245) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - notiffany (0.0.7) - nenv (~> 0.1) - shellany (~> 0.0) nprogress-rails (0.1.2.3) oauth (0.4.7) oauth2 (1.0.0) @@ -648,7 +630,6 @@ GEM sexp_processor (4.6.0) sham_rack (1.3.6) rack - shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) sidekiq (3.3.0) @@ -849,8 +830,6 @@ DEPENDENCIES gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - growl - guard-rspec (~> 4.2.0) haml-rails (~> 0.9.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) @@ -894,8 +873,6 @@ DEPENDENCIES rack-oauth2 (~> 1.0.5) rails (= 4.1.12) raphael-rails (~> 2.1.2) - rb-fsevent - rb-inotify rdoc (~> 3.6) redcarpet (~> 3.3.2) redis-rails (~> 4.0.0) From 2df573dac3859034fcb90566a8ebc270a7e6088a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 9 Oct 2015 09:14:35 -0700 Subject: [PATCH 033/443] Fix bug where merge request comments created by API would not trigger notifications Closes https://github.com/gitlabhq/gitlabhq/issues/9715 --- CHANGELOG | 1 + lib/api/merge_requests.rb | 12 ++++++++++-- spec/requests/api/merge_requests_spec.rb | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f9a4d17fa1..e7e7e7becf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) - Make diff file view easier to use on mobile screens (Stan Hu) + - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu) - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 63ea2f0543..8cfc8ee671 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -249,8 +249,16 @@ module API required_attributes! [:note] merge_request = user_project.merge_requests.find(params[:merge_request_id]) - note = merge_request.notes.new(note: params[:note], project_id: user_project.id) - note.author = current_user + + authorize! :create_note, merge_request + + opts = { + note: params[:note], + noteable_type: 'MergeRequest', + noteable_id: merge_request.id + } + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute if note.save present note, with: Entities::MRNote diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 35b3d3e296..a68c7b1e46 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -379,9 +379,14 @@ describe API::API, api: true do describe "POST /projects/:id/merge_request/:merge_request_id/comments" do it "should return comment" do + original_count = merge_request.notes.size + post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" expect(response.status).to eq(201) expect(json_response['note']).to eq('My comment') + expect(json_response['author']['name']).to eq(user.name) + expect(json_response['author']['username']).to eq(user.username) + expect(merge_request.notes.size).to eq(original_count + 1) end it "should return 400 if note is missing" do From 857cc388e1a93760e37145cd32514dc4026d0775 Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Mon, 12 Oct 2015 18:38:34 +1100 Subject: [PATCH 034/443] Adds modified date to snippets#show #1767 - Fixed a bug with .btn-sm padding. - Changed created_at to modified_at for snippets#index as it seems to be more relevant. --- CHANGELOG | 1 + app/assets/stylesheets/generic/buttons.scss | 10 ++++++++++ app/views/shared/snippets/_snippet.html.haml | 3 +-- app/views/snippets/show.html.haml | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..1e41f6251b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) + - Added last modified date to snippets#show (Han Loong Liauw) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 11acbe3adf..da69fc8f51 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -22,6 +22,12 @@ padding: 11px 24px; } +@mixin btn-sm { + @include btn-default; + @include border-radius(2px); + padding: 5px 10px; +} + @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -71,6 +77,10 @@ @include btn-default; @include btn-white; + &.btn-sm { + @include btn-sm; + } + &.btn-success, &.btn-new, &.btn-create, diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 69a713ad9a..6f4b0453ad 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -17,5 +17,4 @@ = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: '' = snippet.author_name - authored #{time_ago_with_tooltip(snippet.created_at)} - + authored #{time_ago_with_tooltip(snippet.updated_at)} diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 97374e073d..a12cfd0ff4 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -11,12 +11,12 @@ = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do Add new snippet -.append-bottom-10.prepend-top-10 +.append-bottom-10.prepend-top-10.clearfix .pull-right %span.light - created by = link_to user_snippets_path(@snippet.author) do = @snippet.author_name + authored #{time_ago_with_tooltip(@snippet.updated_at)} .back-link - if @snippet.author == current_user From b0164771ec693ff58504ece560371ffec11f9ca9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 11:54:46 +0200 Subject: [PATCH 035/443] Simplify code around (cross)-references --- app/controllers/projects/issues_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/concerns/mentionable.rb | 38 +++++++------- app/models/concerns/participable.rb | 16 +++--- app/models/note.rb | 2 +- app/services/git_push_service.rb | 50 ++++++------------- app/services/issues/create_service.rb | 2 +- app/services/issues/update_service.rb | 2 +- app/services/merge_requests/create_service.rb | 2 +- app/services/merge_requests/update_service.rb | 2 +- lib/gitlab/reference_extractor.rb | 2 + spec/models/concerns/mentionable_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 2 +- spec/support/mentionable_shared_examples.rb | 10 ++-- 14 files changed, 58 insertions(+), 76 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0f89f2e88c..4612abcbae 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -55,7 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController end def show - @participants = @issue.participants(current_user, @project) + @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.inc_author.fresh @noteable = @issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7570934e72..98df6984bf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -246,7 +246,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_show_vars - @participants = @merge_request.participants(current_user, @project) + @participants = @merge_request.participants(current_user) # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 5b0ae41164..694403949c 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -43,53 +43,53 @@ module Mentionable # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. - def has_mentioned?(target) + def cross_reference_exists?(target) SystemNoteService.cross_reference_exists?(target, local_reference) end - def mentioned_users(current_user = nil) - return [] if mentionable_text.blank? - + def all_references(current_user = self.author, text = self.mentionable_text) ext = Gitlab::ReferenceExtractor.new(self.project, current_user) - ext.analyze(mentionable_text) - ext.users.uniq + ext.analyze(text) + ext + end + + def mentioned_users(current_user = nil) + all_references(current_user).users.uniq end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def references(p = project, current_user = self.author, text = mentionable_text) + def referenced_mentionables(current_user = self.author, text = self.mentionable_text) return [] if text.blank? - ext = Gitlab::ReferenceExtractor.new(p, current_user) - ext.analyze(text) - - (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] + refs = all_references(current_user, text) + (refs.issues + refs.merge_requests + refs.commits).uniq - [local_reference] end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. - def create_cross_references!(p = project, a = author, without = []) - refs = references(p) - + def create_cross_references!(author = self.author, without = []) + refs = referenced_mentionables(author) + # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. - refs.reject! { |ref| without.include?(ref) } + refs.reject! { |ref| without.include?(ref) || cross_reference_exists?(ref) } refs.each do |ref| - SystemNoteService.cross_reference(ref, local_reference, a) + SystemNoteService.cross_reference(ref, local_reference, author) end end # When a mentionable field is changed, creates cross-reference notes that # don't already exist - def create_new_cross_references!(p = project, a = author) + def create_new_cross_references!(author = self.author) changes = detect_mentionable_changes return if changes.empty? original_text = changes.collect { |_, vals| vals.first }.join(' ') - preexisting = references(p, self.author, original_text) - create_cross_references!(p, a, preexisting) + preexisting = referenced_mentionables(author, original_text) + create_cross_references!(author, preexisting) end private diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 7c9597333d..d697c125c7 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -37,7 +37,7 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author, project = self.project) + def participants(current_user = self.author) participants = self.class.participant_attrs.flat_map do |attr| meth = method(attr) @@ -48,13 +48,11 @@ module Participable meth.call end - participants_for(value, current_user, project) + participants_for(value, current_user) end.compact.uniq - if project - participants.select! do |user| - user.can?(:read_project, project) - end + participants.select! do |user| + user.can?(:read_project, self.project) end participants @@ -62,14 +60,14 @@ module Participable private - def participants_for(value, current_user = nil, project = nil) + def participants_for(value, current_user = nil) case value when User [value] when Enumerable, ActiveRecord::Relation - value.flat_map { |v| participants_for(v, current_user, project) } + value.flat_map { |v| participants_for(v, current_user) } when Participable - value.participants(current_user, project) + value.participants(current_user) end end end diff --git a/app/models/note.rb b/app/models/note.rb index de3b6df88f..13eb28f571 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -358,7 +358,7 @@ class Note < ActiveRecord::Base end def set_references - create_new_cross_references!(project, author) + create_new_cross_references! end def system? diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index f9a8265d2d..81d47602f1 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -74,48 +74,30 @@ class GitPushService def process_commit_messages(ref) is_default_branch = is_default_branch?(ref) + authors = Hash.new do |hash, commit| + email = commit.author_email + return hash[email] if hash.has_key?(email) + + hash[email] = commit_user(commit) + end + @push_commits.each do |commit| - # Close issues if these commits were pushed to the project's default branch and the commit message matches the - # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to - # a different branch. - issues_to_close = commit.closes_issues(user) - - # Load commit author only if needed. - # For push with 1k commits it prevents 900+ requests in database - author = nil - # Keep track of the issues that will be actually closed because they are on a default branch. # Hence, when creating cross-reference notes, the not-closed issues (on non-default branches) # will also have cross-reference. - actually_closed_issues = [] + closed_issues = [] - if issues_to_close.present? && is_default_branch - author ||= commit_user(commit) - actually_closed_issues = issues_to_close - issues_to_close.each do |issue| - Issues::CloseService.new(project, author, {}).execute(issue, commit) + if is_default_branch + # Close issues if these commits were pushed to the project's default branch and the commit message matches the + # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to + # a different branch. + closed_issues = commit.closes_issues(user) + closed_issues.each do |issue| + Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) end end - if project.default_issues_tracker? - create_cross_reference_notes(commit, actually_closed_issues) - end - end - end - - def create_cross_reference_notes(commit, issues_to_close) - # Create cross-reference notes for any other references than those given in issues_to_close. - # Omit any issues that were referenced in an issue-closing phrase, or have already been - # mentioned from this commit (probably from this commit being pushed to a different branch). - refs = commit.references(project, user) - issues_to_close - refs.reject! { |r| commit.has_mentioned?(r) } - - if refs.present? - author ||= commit_user(commit) - - refs.each do |r| - SystemNoteService.cross_reference(r, commit, author) - end + commit.create_cross_references!(authors[commit], closed_issues) end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 1ea4b72216..bcb380d321 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -10,7 +10,7 @@ module Issues issue.update_attributes(label_ids: label_params) notification_service.new_issue(issue, current_user) event_service.open_issue(issue, current_user) - issue.create_cross_references!(issue.project, current_user) + issue.create_cross_references!(current_user) execute_hooks(issue, 'open') end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 2fc6ef7f35..2b5426ad45 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -35,7 +35,7 @@ module Issues create_title_change_note(issue, issue.previous_changes['title'].first) end - issue.create_new_cross_references!(issue.project, current_user) + issue.create_new_cross_references! execute_hooks(issue, 'update') end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 9651b16462..009d5a6867 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -18,7 +18,7 @@ module MergeRequests merge_request.update_attributes(label_ids: label_params) event_service.open_mr(merge_request, current_user) notification_service.new_merge_request(merge_request, current_user) - merge_request.create_cross_references!(merge_request.project, current_user) + merge_request.create_cross_references!(current_user) execute_hooks(merge_request) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 25d79e22e3..ebbe0af803 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -59,7 +59,7 @@ module MergeRequests merge_request.mark_as_unchecked end - merge_request.create_new_cross_references!(merge_request.project, current_user) + merge_request.create_new_cross_references! execute_hooks(merge_request, 'update') end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 0961bd8042..30497e274c 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -39,6 +39,8 @@ module Gitlab # # Returns the results Array for the requested filter type def pipeline_result(filter_type) + return [] if @text.blank? + klass = filter_type.to_s.camelize + 'ReferenceFilter' filter = Gitlab::Markdown.const_get(klass) diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 2d6fe00321..6179882e93 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -25,7 +25,7 @@ describe Issue, "Mentionable" do it 'correctly removes already-mentioned Commits' do expect(SystemNoteService).not_to receive(:cross_reference) - issue.create_cross_references!(project, author, [commit2]) + issue.create_cross_references!(author, [commit2]) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index c483060fd7..bb62078341 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -155,7 +155,7 @@ describe GitPushService do before do allow(commit).to receive_messages( - safe_message: "this commit \n mentions ##{issue.id}", + safe_message: "this commit \n mentions ##{issue.iid}", references: [issue], author_name: commit_author.name, author_email: commit_author.email diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index e3de0afb44..220566a22b 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -65,7 +65,7 @@ shared_examples 'a mentionable' do it "extracts references from its reference property" do # De-duplicate and omit itself - refs = subject.references(project) + refs = subject.referenced_mentionables expect(refs.size).to eq(6) expect(refs).to include(mentioned_issue) expect(refs).to include(mentioned_mr) @@ -84,14 +84,14 @@ shared_examples 'a mentionable' do with(referenced, subject.local_reference, author) end - subject.create_cross_references!(project, author) + subject.create_cross_references! end it 'detects existing cross-references' do SystemNoteService.cross_reference(mentioned_issue, subject.local_reference, author) - expect(subject).to have_mentioned(mentioned_issue) - expect(subject).not_to have_mentioned(mentioned_mr) + expect(subject.cross_reference_exists?(mentioned_issue)).to be_truthy + expect(subject.cross_reference_exists?(mentioned_mr)).to be_falsey end end @@ -143,6 +143,6 @@ shared_examples 'an editable mentionable' do end set_mentionable_text.call(new_text) - subject.create_new_cross_references!(project, author) + subject.create_new_cross_references!(author) end end From b8f5e7427f7cbcdb19dcc5554301e87a880cfe2a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 12 Oct 2015 12:07:31 +0200 Subject: [PATCH 036/443] Show notifications button even if user is not member of a project Notifications button was unavailable if user wasn't member of the project, even if protected project is available via group privileges. Showing disabled button with explanation tool-tip is less confusing. This closes #2846. --- app/controllers/projects_controller.rb | 1 + .../projects/buttons/_notifications.html.haml | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 213c2a7173..ffbd91324c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -88,6 +88,7 @@ class ProjectsController < ApplicationController else if current_user @membership = @project.project_member_by_id(current_user.id) + @group_member = GroupMember.find_by(user_id: current_user.id) end render :show diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 4b69a6d7a6..501a51d0e8 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,14 +1,21 @@ -- return unless @membership +- return unless [@membership, @group_member].any? -= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do - = hidden_field_tag :notification_type, 'project' - = hidden_field_tag :notification_id, @membership.id - = hidden_field_tag :notification_level - %span.dropdown - %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} - = icon('bell') - = notification_label(@membership) - = icon('angle-down') - %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - Notification.project_notification_levels.each do |level| - = notification_list_item(level, @membership) +- if @membership + = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do + = hidden_field_tag :notification_type, 'project' + = hidden_field_tag :notification_id, @membership.id + = hidden_field_tag :notification_level + %span.dropdown + %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} + = icon('bell') + = notification_label(@membership) + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + - Notification.project_notification_levels.each do |level| + = notification_list_item(level, @membership) + +- elsif @group_member + .btn.btn-new.disabled#notifications-button + = icon('bell') + = notification_label(@group_member) + = icon('angle-down') From 27d952b1197f2dc615c383c21eb287313d81c74c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 14:30:44 +0200 Subject: [PATCH 037/443] Fix cross-references originating from notes --- app/models/concerns/mentionable.rb | 4 ++-- app/models/note.rb | 13 +++---------- app/services/notes/create_service.rb | 8 +------- app/services/notes/update_service.rb | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 694403949c..5f53ea2563 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -66,8 +66,8 @@ module Mentionable end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. - def create_cross_references!(author = self.author, without = []) - refs = referenced_mentionables(author) + def create_cross_references!(author = self.author, without = [], text = self.mentionable_text) + refs = referenced_mentionables(author, text) # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the diff --git a/app/models/note.rb b/app/models/note.rb index 13eb28f571..ee0c14598f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -62,7 +62,6 @@ class Note < ActiveRecord::Base serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } - after_update :set_references class << self def discussions_from_notes(notes) @@ -333,15 +332,13 @@ class Note < ActiveRecord::Base end def noteable_type_name - if noteable_type.present? - noteable_type.downcase - end + noteable_type.downcase if noteable_type.present? end # FIXME: Hack for polymorphic associations with STI # For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations - def noteable_type=(sType) - super(sType.to_s.classify.constantize.base_class.to_s) + def noteable_type=(noteable_type) + super(noteable_type.to_s.classify.constantize.base_class.to_s) end # Reset notes events cache @@ -357,10 +354,6 @@ class Note < ActiveRecord::Base Event.reset_event_cache_for(self) end - def set_references - create_new_cross_references! - end - def system? read_attribute(:system) end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 482c044404..2001dc89c3 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -11,13 +11,7 @@ module Notes # Skip system notes, like status changes and cross-references. unless note.system event_service.leave_note(note, note.author) - - # Create a cross-reference note if this Note contains GFM that names an - # issue, merge request, or commit. - note.references.each do |mentioned| - SystemNoteService.cross_reference(mentioned, note.noteable, note.author) - end - + note.create_cross_references! execute_hooks(note) end end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index c22a9333ef..6c2f08e596 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -4,7 +4,7 @@ module Notes return note unless note.editable? note.update_attributes(params.merge(updated_by: current_user)) - + note.create_new_cross_references! note.reset_events_cache note From 41075bad8daeed07bceaa44cbfe54ab1b470ca87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 14:39:33 +0200 Subject: [PATCH 038/443] Chaining ftw --- app/models/concerns/participable.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index d697c125c7..2218244597 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -49,13 +49,9 @@ module Participable end participants_for(value, current_user) - end.compact.uniq - - participants.select! do |user| + end.compact.uniq.select do |user| user.can?(:read_project, self.project) end - - participants end private From 7a0cc665ff5ad3969f36082baa162a2169c34612 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 15:34:08 +0200 Subject: [PATCH 039/443] Remove useless assignment --- app/models/concerns/participable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 2218244597..ffc874357f 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -38,7 +38,7 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request def participants(current_user = self.author) - participants = self.class.participant_attrs.flat_map do |attr| + self.class.participant_attrs.flat_map do |attr| meth = method(attr) value = From 6f35614852a7cf360bbc4af6ba6c450d8667c04e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 16:23:15 +0200 Subject: [PATCH 040/443] Fix mentionable specs --- spec/models/commit_spec.rb | 4 ++-- spec/models/issue_spec.rb | 2 +- spec/models/note_spec.rb | 5 ++--- spec/support/mentionable_shared_examples.rb | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e303a97e6b..90be932495 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -89,9 +89,9 @@ eos end it_behaves_like 'a mentionable' do - subject { commit } + subject { create(:project).commit } - let(:author) { create(:user, email: commit.author_email) } + let(:author) { create(:user, email: subject.author_email) } let(:backref_text) { "commit #{subject.id}" } let(:set_mentionable_text) do ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index cf336d8295..623332cd2f 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -69,7 +69,7 @@ describe Issue do end it_behaves_like 'an editable mentionable' do - subject { create(:issue, project: project) } + subject { create(:issue) } let(:backref_text) { "issue #{subject.to_reference}" } let(:set_mentionable_text) { ->(txt){ subject.description = txt } } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 3a0b194ba1..75564839dc 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -192,10 +192,9 @@ describe Note do end it_behaves_like 'an editable mentionable' do - subject { create :note, noteable: issue, project: project } + subject { create :note, noteable: issue, project: issue.project } - let(:project) { create(:project) } - let(:issue) { create :issue, project: project } + let(:issue) { create :issue } let(:backref_text) { issue.gfm_reference } let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 220566a22b..412c6f4ead 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -5,7 +5,7 @@ # - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } } def common_mentionable_setup - let(:project) { create :project } + let(:project) { subject.project } let(:author) { subject.author } let(:mentioned_issue) { create(:issue, project: project) } From d02d02c672bcac0d2ef46204d132645bc69827a8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 12 Oct 2015 21:43:24 -0700 Subject: [PATCH 041/443] Fix error preventing displaying of commit data for a directory with a leading dot Closes https://github.com/gitlabhq/gitlabhq/issues/8763 --- CHANGELOG | 1 + app/controllers/projects/refs_controller.rb | 7 +++++++ config/routes.rb | 4 +++- features/project/source/browse_files.feature | 6 ++++++ features/steps/project/source/browse_files.rb | 13 +++++++++++++ spec/support/test_env.rb | 2 +- 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..d802fb8db4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) + - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 6080c849c8..c4e18c1707 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -3,6 +3,7 @@ class Projects::RefsController < Projects::ApplicationController include TreeHelper before_action :require_non_empty_project + before_action :validate_ref_id before_action :assign_ref_vars before_action :authorize_download_code! @@ -71,4 +72,10 @@ class Projects::RefsController < Projects::ApplicationController format.js end end + + private + + def validate_ref_id + return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex + end end diff --git a/config/routes.rb b/config/routes.rb index 8e6fbf6340..893ab59c32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -543,8 +543,10 @@ Gitlab::Application.routes.draw do member do # tree viewer logs get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + # Directories with leading dots erroneously get rejected if git + # ref regex used in constraints. Regex verification now done in controller. get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { - id: Gitlab::Regex.git_reference_regex, + id: /.*/, path: /.*/ } end diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 377c5e1a9a..6b0484b6a3 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -205,3 +205,9 @@ Feature: Project Source Browse Files And I see the ref 'test' has been selected And I visit the 'test' tree Then I see the commit data + + @javascript + Scenario: I browse code with a leading dot in the directory + Given I switch ref to fix + And I visit the fix tree + Then I see the commit data for a directory with a leading dot diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index cb100ca0f5..1b27500497 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -286,6 +286,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps select "'test'", from: 'ref' end + step "I switch ref to fix" do + select "fix", from: 'ref' + end + step "I see the ref 'test' has been selected" do expect(page).to have_selector '.select2-chosen', text: "'test'" end @@ -294,11 +298,20 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps visit namespace_project_tree_path(@project.namespace, @project, "'test'") end + step "I visit the fix tree" do + visit namespace_project_tree_path(@project.namespace, @project, "fix/.testdir") + end + step 'I see the commit data' do expect(page).to have_css('.tree-commit-link', visible: true) expect(page).not_to have_content('Loading commit data...') end + step 'I see the commit data for a directory with a leading dot' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + private def set_new_content diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3eab74ba98..d12ba25b71 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -9,7 +9,7 @@ module TestEnv 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', - 'fix' => '12d65c8', + 'fix' => '48f0be4', 'improve/awesome' => '5937ac0', 'markdown' => '0ed8c6c', 'master' => '5937ac0', From 127f288afbc454975bee910cee22f330a86c0e1b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 09:37:42 +0000 Subject: [PATCH 042/443] Use `to_reference` where possible. --- spec/services/git_push_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index bb62078341..93bac384a6 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -155,7 +155,7 @@ describe GitPushService do before do allow(commit).to receive_messages( - safe_message: "this commit \n mentions ##{issue.iid}", + safe_message: "this commit \n mentions #{issue.to_reference}", references: [issue], author_name: commit_author.name, author_email: commit_author.email @@ -272,4 +272,4 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, new_ref) end end -end +end \ No newline at end of file From df99ddbba13db4a7699bf1d585675f921cf382ce Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Tue, 13 Oct 2015 21:24:44 +1100 Subject: [PATCH 043/443] Adds ability to remove the forked relationship This was previously possible through the API but can now be done through the project#edit settings screen if the current user is the owner of the project. Update changelog --- CHANGELOG | 2 + app/controllers/projects_controller.rb | 9 ++++- app/helpers/projects_helper.rb | 4 ++ app/models/ability.rb | 3 +- app/views/projects/edit.html.haml | 16 ++++++++ app/views/projects/remove_fork.js.haml | 2 + config/routes.rb | 1 + lib/api/projects.rb | 2 +- spec/controllers/projects_controller_spec.rb | 40 ++++++++++++++++++++ spec/features/projects_spec.rb | 29 ++++++++++++-- 10 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 app/views/projects/remove_fork.js.haml diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..3c730aef5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,8 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) + - Adds ability to remove the forked relationship from project settings + screen. #2578 (Han Loong Liauw) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 213c2a7173..1fb83d0eb6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController before_action :repository, except: [:new, :create] # Authorize - before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] + before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive, :remove_fork] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -64,6 +64,13 @@ class ProjectsController < ApplicationController end end + def remove_fork + if @project.forked? + @project.forked_project_link.destroy + flash[:notice] = 'Fork relationship has been removed.' + end + end + def activity respond_to do |format| format.html diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a0220af4c3..b0a3a20fa0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -70,6 +70,10 @@ module ProjectsHelper "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" end + def remove_fork_project_message(project) + "You are going to remove the fork relationship to the source project from #{@project.forked_from_project.namespace.try(:name)}. Are you ABSOLUTELY sure?" + end + def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end diff --git a/app/models/ability.rb b/app/models/ability.rb index a020b24a55..11ada46610 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -185,7 +185,8 @@ class Ability :change_visibility_level, :rename_project, :remove_project, - :archive_project + :archive_project, + :remove_fork_project ] end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 1882a82fba..ce77a9242f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -189,6 +189,21 @@ - else .nothing-here-block Only the project owner can transfer a project + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| + .panel.panel-default.panel.panel-danger + .panel-heading Remove forked relationship + .panel-body + %p + This will remove the relationship to the source project from + = link_to project_path(@project.forked_from_project) do + = @project.forked_from_project.namespace.try(:name) + %br + %strong Once removed it cannot be reversed through this interface + = button_to 'Remove forked relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - elsif @project.forked? + .nothing-here-block Only the project owner can remove the fork relationship + - if can?(current_user, :remove_project, @project) .panel.panel-default.panel.panel-danger .panel-heading Remove project @@ -203,6 +218,7 @@ - else .nothing-here-block Only project owner can remove a project + .save-project-loader.hide .center %h2 diff --git a/app/views/projects/remove_fork.js.haml b/app/views/projects/remove_fork.js.haml new file mode 100644 index 0000000000..17b9fecfeb --- /dev/null +++ b/app/views/projects/remove_fork.js.haml @@ -0,0 +1,2 @@ +:plain + location.href = "#{edit_namespace_project_path(@project.namespace, @project)}"; diff --git a/config/routes.rb b/config/routes.rb index 8e6fbf6340..3ac4342b23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -378,6 +378,7 @@ Gitlab::Application.routes.draw do [:new, :create, :index], path: "/") do member do put :transfer + put :remove_fork post :archive post :unarchive post :toggle_star diff --git a/lib/api/projects.rb b/lib/api/projects.rb index c2fb36b414..e8a0e7f3ec 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -247,7 +247,7 @@ module API # DELETE /projects/:id/fork delete ":id/fork" do authenticated_as_admin! - unless user_project.forked_project_link.nil? + if user_project.forked? user_project.forked_project_link.destroy end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 21beaf37fc..626b56cc78 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -62,4 +62,44 @@ describe ProjectsController do expect(user.starred?(public_project)).to be_falsey end end + + describe "PUT remove_fork" do + context 'when signed in' do + before do + sign_in(user) + end + + context 'with forked project' do + let(:project_fork) { create(:project, namespace: user.namespace) } + + it 'should remove fork from project' do + create(:forked_project_link, forked_to_project: project_fork) + put(:remove_fork, + namespace_id: project_fork.namespace.to_param, + id: project_fork.to_param, format: :js) + + expect(project_fork.forked?).to be_falsey + expect(flash[:notice]).to eq('Fork relationship has been removed.') + expect(response).to render_template(:remove_fork) + end + end + + it 'should do nothing if project was not forked' do + unforked_project = create(:project, namespace: user.namespace) + put(:remove_fork, + namespace_id: unforked_project.namespace.to_param, + id: unforked_project.to_param, format: :js) + + expect(flash[:notice]).to be_nil + expect(response).to render_template(:remove_fork) + end + end + + it "does nothing if user is not signed in" do + put(:remove_fork, + namespace_id: project.namespace.to_param, + id: project.to_param, format: :js) + expect(response.status).to eq(401) + end + end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index aac93b17a3..df0dcb2bb2 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -34,6 +34,27 @@ feature 'Project', feature: true do end end + describe 'remove forked relationship', js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + login_with user + create(:forked_project_link, forked_to_project: project) + visit edit_namespace_project_path(project.namespace, project) + end + + it 'should remove fork' do + expect(page).to have_content 'Remove forked relationship' + + remove_with_confirm('Remove forked relationship', project.path) + + expect(page).to have_content 'Fork relationship has been removed.' + expect(project.forked?).to be_falsey + expect(page).not_to have_content 'Remove forked relationship' + end + end + describe 'removal', js: true do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } @@ -45,13 +66,13 @@ feature 'Project', feature: true do end it 'should remove project' do - expect { remove_project }.to change {Project.count}.by(-1) + expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) end end - def remove_project - click_button "Remove project" - fill_in 'confirm_name_input', with: project.path + def remove_with_confirm(button_text, confirm_with) + click_button button_text + fill_in 'confirm_name_input', with: confirm_with click_button 'Confirm' end end From 712d17684b2b9a8664cdff685c44fa59ea6fabbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 13:07:59 +0200 Subject: [PATCH 044/443] Make Reply by email easier to configure --- .gitignore | 1 - Gemfile | 2 +- Gemfile.lock | 4 +- config/gitlab.yml.example | 24 ++- config/initializers/1_settings.rb | 4 +- config/mail_room.yml | 39 ++++ config/mail_room.yml.example | 39 ---- doc/incoming_email/README.md | 307 +++++++++++++----------------- lib/gitlab/incoming_email.rb | 4 +- lib/tasks/gitlab/check.rake | 37 +--- 10 files changed, 203 insertions(+), 258 deletions(-) create mode 100644 config/mail_room.yml delete mode 100644 config/mail_room.yml.example diff --git a/.gitignore b/.gitignore index 2a97eacad4..73bde4cc76 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ config/initializers/rack_attack.rb config/initializers/smtp_settings.rb config/resque.yml config/unicorn.rb -config/mail_room.yml config/secrets.yml coverage/* db/*.sqlite3 diff --git a/Gemfile b/Gemfile index 967092994a..392644dfa8 100644 --- a/Gemfile +++ b/Gemfile @@ -290,7 +290,7 @@ gem 'newrelic-grape' gem 'octokit', '~> 3.7.0' -gem "mail_room", "~> 0.6.0" +gem "mail_room", "~> 0.6.1" gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index 58426a6068..7e989aa461 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -392,7 +392,7 @@ GEM systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) - mail_room (0.6.0) + mail_room (0.6.1) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) @@ -854,7 +854,7 @@ DEPENDENCIES jquery-ui-rails (~> 4.2.1) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) - mail_room (~> 0.6.0) + mail_room (~> 0.6.1) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 4f7f0b6ef1..8b85981497 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -99,7 +99,29 @@ production: &base # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html incoming_email: enabled: false - address: "incoming+%{key}@gitlab.example.com" + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + address: "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "imap.gmail.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4c78bd6e2f..f04263c760 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -187,7 +187,9 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci[ # Reply by email # Settings['incoming_email'] ||= Settingslogic.new({}) -Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? +Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? +Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil? +Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? # # Gravatar diff --git a/config/mail_room.yml b/config/mail_room.yml new file mode 100644 index 0000000000..42f6f74c46 --- /dev/null +++ b/config/mail_room.yml @@ -0,0 +1,39 @@ +:mailboxes: +<% +require_relative 'config/environment.rb' + +if Gitlab::IncomingEmail.enabled? + config = Gitlab::IncomingEmail.config + + redis_config_file = "config/resque.yml" + redis_url = + if File.exists?(redis_config_file) + YAML.load_file(redis_config_file)[Rails.env] + else + "redis://localhost:6379" + end + %> + - + :host: <%= config.host.to_json %> + :port: <%= config.port.to_json %> + :ssl: <%= config.ssl.to_json %> + :start_tls: <%= config.start_tls.to_json %> + :email: <%= config.user.to_json %> + :password: <%= config.password.to_json %> + + :name: <%= config.mailbox.to_json %> + + :delete_after_delivery: true + + :delivery_method: sidekiq + :delivery_options: + :redis_url: <%= redis_url.to_json %> + :namespace: resque:gitlab + :queue: incoming_email + :worker: EmailReceiverWorker + + :arbitration_method: redis + :arbitration_options: + :redis_url: <%= redis_url.to_json %> + :namespace: mail_room:gitlab +<% end %> diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example deleted file mode 100644 index bb624e8a18..0000000000 --- a/config/mail_room.yml.example +++ /dev/null @@ -1,39 +0,0 @@ -:mailboxes: - - - # # IMAP server host - # :host: "imap.gmail.com" - # # IMAP server port - # :port: 993 - # # Whether the IMAP server uses SSL - # :ssl: true - # # Whether the IMAP server uses StartTLS - # :start_tls: false - # # Email account username. Usually the full email address. - # :email: "gitlab-incoming@gmail.com" - # # Email account password - # :password: "password" - - # # The name of the mailbox where incoming mail will end up. Usually "inbox". - # :name: "inbox" - - # # Always "sidekiq". - # :delivery_method: sidekiq - # # Always true. - # :delete_after_delivery: true - # :delivery_options: - # # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - # :redis_url: redis://localhost:6379 - # # Always "resque:gitlab". - # :namespace: resque:gitlab - # # Always "incoming_email". - # :queue: incoming_email - # # Always "EmailReceiverWorker". - # :worker: EmailReceiverWorker - - # # Always "redis". - # :arbitration_method: redis - # :arbitration_options: - # # The URL to the Redis server. Should match the URL in config/resque.yml. - # :redis_url: redis://localhost:6379 - # # Always "mail_room:gitlab". - # :namespace: mail_room:gitlab diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index aafa2345fa..86d205ba7a 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -4,9 +4,9 @@ GitLab can be set up to allow users to comment on issues and merge requests by r ## Get a mailbox -Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. +Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. -If you want to use Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). +If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). @@ -14,30 +14,62 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ### Omnibus package installations -1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `key` that references the item being replied to and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: ```ruby - # Postfix mail server, assumes mailbox incoming@gitlab.example.com + # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com gitlab_rails['incoming_email_enabled'] = true + + # The email address including a placeholder for the key that references the item being replied to. + # The `%{key}` placeholder is added after the user part, before the `@`. gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" - gitlab_rails['incoming_email_host'] = "gitlab.example.com" # IMAP server host - gitlab_rails['incoming_email_port'] = 143 # IMAP server port - gitlab_rails['incoming_email_ssl'] = false # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_email'] = "incoming" # Email account username. Usually the full email address. - gitlab_rails['incoming_email_password'] = "[REDACTED]" # Email account password - gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox". + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + gitlab_rails['incoming_email_email'] = "incoming" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "gitlab.example.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 143 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = false + # Whether the IMAP server uses StartTLS + gitlab_rails['incoming_email_start_tls'] = false + + # The mailbox where incoming mail will end up. Usually "inbox". + gitlab_rails['incoming_email_mailbox_name'] = "inbox" ``` ```ruby - # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com + # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com gitlab_rails['incoming_email_enabled'] = true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" - gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server host - gitlab_rails['incoming_email_port'] = 993 # IMAP server port - gitlab_rails['incoming_email_ssl'] = true # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account username. Usually the full email address. - gitlab_rails['incoming_email_password'] = "[REDACTED]" # Email account password - gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox". + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "imap.gmail.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 993 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = true + # Whether the IMAP server uses StartTLS + gitlab_rails['incoming_email_start_tls'] = false + + # The mailbox where incoming mail will end up. Usually "inbox". + gitlab_rails['incoming_email_mailbox_name'] = "inbox" ``` As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. @@ -64,229 +96,146 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these cd /home/git/gitlab ``` -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to: +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: ```sh sudo editor config/gitlab.yml ``` ```yaml - # Postfix mail server, assumes mailbox incoming@gitlab.example.com + # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com incoming_email: enabled: true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. address: "incoming+%{key}@gitlab.example.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "incoming" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "gitlab.example.com" + # IMAP server port + port: 143 + # Whether the IMAP server uses SSL + ssl: false + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" ``` ```yaml - # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com + # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. address: "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "imap.gmail.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" ``` As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. -2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`: - - ```sh - sudo cp config/mail_room.yml.example config/mail_room.yml - ``` - -3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: - - ```sh - sudo editor config/mail_room.yml - ``` - - ```yaml - # Postfix mail server - :mailboxes: - - - # IMAP server host - :host: "gitlab.example.com" - # IMAP server port - :port: 143 - # Whether the IMAP server uses SSL - :ssl: false - # Whether the IMAP server uses StartTLS - :start_tls: false - # Email account username. Usually the full email address. - :email: "incoming" - # Email account password - :password: "[REDACTED]" - - # The name of the mailbox where incoming mail will end up. Usually "inbox". - :name: "inbox" - - # Always "sidekiq". - :delivery_method: sidekiq - # Always true. - :delete_after_delivery: true - :delivery_options: - # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "resque:gitlab". - :namespace: resque:gitlab - # Always "incoming_email". - :queue: incoming_email - # Always "EmailReceiverWorker" - :worker: EmailReceiverWorker - - # Always "redis". - :arbitration_method: redis - :arbitration_options: - # The URL to the Redis server. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "mail_room:gitlab". - :namespace: mail_room:gitlab - ``` - - ```yaml - # Gmail / Google Apps - :mailboxes: - - - # IMAP server host - :host: "imap.gmail.com" - # IMAP server port - :port: 993 - # Whether the IMAP server uses SSL - :ssl: true - # Whether the IMAP server uses StartTLS - :start_tls: false - # Email account username. Usually the full email address. - :email: "gitlab-incoming@gmail.com" - # Email account password - :password: "[REDACTED]" - - # The name of the mailbox where incoming mail will end up. Usually "inbox". - :name: "inbox" - - # Always "sidekiq". - :delivery_method: sidekiq - # Always true. - :delete_after_delivery: true - :delivery_options: - # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "resque:gitlab". - :namespace: resque:gitlab - # Always "incoming_email". - :queue: incoming_email - # Always "EmailReceiverWorker" - :worker: EmailReceiverWorker - - # Always "redis". - :arbitration_method: redis - :arbitration_options: - # The URL to the Redis server. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "mail_room:gitlab". - :namespace: mail_room:gitlab - ``` - -5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`: +1. Enable `mail_room` in the init script at `/etc/default/gitlab`: ```sh sudo mkdir -p /etc/default echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab ``` -6. Restart GitLab: +1. Restart GitLab: ```sh sudo service gitlab restart ``` -7. Verify that everything is configured correctly: +1. Verify that everything is configured correctly: ```sh sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production ``` -8. Reply by email should now be working. +1. Reply by email should now be working. ### Development 1. Go to the GitLab installation directory. -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to: +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: ```yaml - # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com + # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true + + # The email address including a placeholder for the key that references the item being replied to. + # The `%{key}` placeholder is added after the user part, before the `@`. address: "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "imap.gmail.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" ``` As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`. -2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`: - - ```sh - sudo cp config/mail_room.yml.example config/mail_room.yml - ``` - -3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: - - ```yaml - # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com - :mailboxes: - - - # IMAP server host - :host: "imap.gmail.com" - # IMAP server port - :port: 993 - # Whether the IMAP server uses SSL - :ssl: true - # Whether the IMAP server uses StartTLS - :start_tls: false - # Email account username. Usually the full email address. - :email: "gitlab-incoming@gmail.com" - # Email account password - :password: "[REDACTED]" - - # The name of the mailbox where incoming mail will end up. Usually "inbox". - :name: "inbox" - - # Always "sidekiq". - :delivery_method: sidekiq - # Always true. - :delete_after_delivery: true - :delivery_options: - # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "resque:gitlab". - :namespace: resque:gitlab - # Always "incoming_email". - :queue: incoming_email - # Always "EmailReceiverWorker" - :worker: EmailReceiverWorker - - # Always "redis". - :arbitration_method: redis - :arbitration_options: - # The URL to the Redis server. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "mail_room:gitlab". - :namespace: mail_room:gitlab - ``` - -4. Uncomment the `mail_room` line in your `Procfile`: +1. Uncomment the `mail_room` line in your `Procfile`: ```yaml mail_room: bundle exec mail_room -q -c config/mail_room.yml ``` -6. Restart GitLab: +1. Restart GitLab: ```sh bundle exec foreman start ``` -7. Verify that everything is configured correctly: +1. Verify that everything is configured correctly: ```sh bundle exec rake gitlab:incoming_email:check RAILS_ENV=development ``` -8. Reply by email should now be working. +1. Reply by email should now be working. diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 856ccc7108..9068d79c95 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -24,12 +24,12 @@ module Gitlab match[1] end - private - def config Gitlab.config.incoming_email end + private + def address_regex wildcard_address = config.address return nil unless wildcard_address diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 66f1ecf385..606bf241db 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -642,7 +642,6 @@ namespace :gitlab do if Gitlab.config.incoming_email.enabled check_address_formatted_correctly - check_mail_room_config_exists check_imap_authentication if Rails.env.production? @@ -744,42 +743,16 @@ namespace :gitlab do end end - def check_mail_room_config_exists - print "MailRoom config exists? ... " - - mail_room_config_file = Rails.root.join("config", "mail_room.yml") - - if File.exists?(mail_room_config_file) - puts "yes".green - else - puts "no".red - try_fixing_it( - "Copy config/mail_room.yml.example to config/mail_room.yml", - "Check that the information in config/mail_room.yml is correct" - ) - for_more_information( - "doc/incoming_email/README.md" - ) - fix_and_rerun - end - end - def check_imap_authentication print "IMAP server credentials are correct? ... " - mail_room_config_file = Rails.root.join("config", "mail_room.yml") - - unless File.exists?(mail_room_config_file) - puts "can't check because of previous errors".magenta - return - end - - config = YAML.load_file(mail_room_config_file)[:mailboxes].first rescue nil + config = Gitlab.config.incoming_email if config begin - imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) - imap.login(config[:email], config[:password]) + imap = Net::IMAP.new(config.host, port: config.port, ssl: config.ssl) + imap.starttls if config.start_tls + imap.login(config.user, config.password) connected = true rescue connected = false @@ -791,7 +764,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - "Check that the information in config/mail_room.yml is correct" + "Check that the information in config/gitlab.yml is correct" ) for_more_information( "doc/incoming_email/README.md" From 5dd77358f6293249494bf390140843f63dfd220a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 13:36:47 +0200 Subject: [PATCH 045/443] Pass project to RedactorFilter --- app/helpers/gitlab_markdown_helper.rb | 4 ++-- lib/gitlab/markdown.rb | 11 ++++++++--- lib/gitlab/markdown/reference_filter.rb | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1b8bb46d25..6581348212 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -59,7 +59,7 @@ module GitlabMarkdownHelper user = current_user if defined?(current_user) html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], user: user) + Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user) end # TODO (rspeicher): Remove all usages of this helper and just call `markdown` @@ -78,7 +78,7 @@ module GitlabMarkdownHelper user = current_user if defined?(current_user) html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], user: user) + Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) end def asciidoc(text) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index f389d0a80d..d5b0060dd5 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -47,12 +47,17 @@ module Gitlab # # html - String to process # options - Hash of options to customize output - # :pipeline - Symbol pipeline type - # :user - User object + # :pipeline - Symbol pipeline type + # :project - Project + # :user - User object # # Returns an HTML-safe String def self.post_process(html, options) - doc = post_processor.to_document(html, current_user: options[:user]) + context = { + project: options[:project], + current_user: options[:user] + } + doc = post_processor.to_document(html, context) if options[:pipeline] == :atom doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index c7d4b15d45..0ea966c744 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -15,7 +15,7 @@ module Gitlab def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i - return true if project_id == context[:project].id + return true if project_id == context[:project].try(:id) project = Project.find(project_id) rescue nil Ability.abilities.allowed?(user, :read_project, project) From 251e13666d04a1c8427401962e3e171e569d9088 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 18:23:49 +0200 Subject: [PATCH 046/443] Efficiently load multiple references of one type. --- lib/gitlab/markdown/issue_reference_filter.rb | 5 +--- lib/gitlab/markdown/label_reference_filter.rb | 5 +--- .../merge_request_reference_filter.rb | 5 +--- lib/gitlab/markdown/reference_filter.rb | 2 ++ .../markdown/reference_gatherer_filter.rb | 29 +++++++++++++++++-- .../markdown/snippet_reference_filter.rb | 5 +--- lib/gitlab/markdown/user_reference_filter.rb | 5 +--- 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index cd2a9b7568..481d282f7b 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -28,10 +28,7 @@ module Gitlab end def self.referenced_by(node) - issue = Issue.find(node.attr("data-issue")) rescue nil - return unless issue - - { issue: issue } + { issue: LazyReference.new(Issue, node.attr("data-issue")) } end def call diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 59568ab531..618acb7a57 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -23,10 +23,7 @@ module Gitlab end def self.referenced_by(node) - label = Label.find(node.attr("data-label")) rescue nil - return unless label - - { label: label } + { label: LazyReference.new(Label, node.attr("data-label")) } end def call diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 440574e574..5bc6326980 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -28,10 +28,7 @@ module Gitlab end def self.referenced_by(node) - merge_request = MergeRequest.find(node.attr("data-merge-request")) rescue nil - return unless merge_request - - { merge_request: merge_request } + { merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) } end def call diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 0ea966c744..0ae0d93f70 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -12,6 +12,8 @@ module Gitlab # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. class ReferenceFilter < HTML::Pipeline::Filter + LazyReference = Struct.new(:klass, :ids) + def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index 89acb312cd..cf9a2303db 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -12,7 +12,8 @@ module Gitlab def initialize(*) super - result[:references] ||= Hash.new { |hash, type| hash[type] = [] } + result[:lazy_references] ||= Hash.new { |hash, type| hash[type] = [] } + result[:references] ||= Hash.new { |hash, type| hash[type] = [] } end def call @@ -20,6 +21,8 @@ module Gitlab gather_references(node) end + load_lazy_references + doc end @@ -35,9 +38,29 @@ module Gitlab references = reference_filter.referenced_by(node) return unless references - + references.each do |type, values| - result[:references][type].push(*values) + Array.wrap(values).each do |value| + refs = + if value.is_a?(ReferenceFilter::LazyReference) + result[:lazy_references] + else + result[:references] + end + + refs[type] << value + end + end + end + + # Will load all references of one type using one query. + def load_lazy_references + result[:lazy_references].each do |type, refs| + refs.group_by(&:klass).each do |klass, refs| + ids = refs.map(&:ids).flatten + values = klass.find(ids) + result[:references][type].push(*values) + end end end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index a7396e9652..f783f95171 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -28,10 +28,7 @@ module Gitlab end def self.referenced_by(node) - snippet = Snippet.find(node.attr("data-snippet")) rescue nil - return unless snippet - - { snippet: snippet } + { snippet: LazyReference.new(Snippet, node.attr("data-snippet")) } end def call diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 4567e98369..2a594e1662 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -30,10 +30,7 @@ module Gitlab { user: group.users } elsif node.has_attribute?('data-user') - user = User.find(node.attr('data-user')) rescue nil - return unless user - - { user: user } + { user: LazyReference.new(User, node.attr('data-user')) } elsif node.has_attribute?('data-project') project = Project.find(node.attr('data-project')) rescue nil return unless project From 93fcddd7a7ce4ed259794a4511ae04035ae33be2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 22:53:05 +0200 Subject: [PATCH 047/443] Allow ReferenceExtractor to efficiently load references from multiple texts at once --- lib/gitlab/markdown/reference_filter.rb | 9 +++++++- .../markdown/reference_gatherer_filter.rb | 9 +++----- lib/gitlab/reference_extractor.rb | 23 +++++++++++++++---- spec/lib/gitlab/reference_extractor_spec.rb | 14 +++++------ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 0ae0d93f70..ede9f8865f 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -12,7 +12,14 @@ module Gitlab # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. class ReferenceFilter < HTML::Pipeline::Filter - LazyReference = Struct.new(:klass, :ids) + LazyReference = Struct.new(:klass, :ids) do + def self.load(refs) + refs.group_by(&:klass).flat_map do |klass, refs| + ids = refs.flat_map(&:ids) + klass.where(id: ids) + end + end + end def self.user_can_reference?(user, node, context) if node.has_attribute?('data-project') diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index cf9a2303db..18df5db94d 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -21,7 +21,7 @@ module Gitlab gather_references(node) end - load_lazy_references + load_lazy_references unless context[:load_lazy_references] == false doc end @@ -56,11 +56,8 @@ module Gitlab # Will load all references of one type using one query. def load_lazy_references result[:lazy_references].each do |type, refs| - refs.group_by(&:klass).each do |klass, refs| - ids = refs.map(&:ids).flatten - values = klass.find(ids) - result[:references][type].push(*values) - end + values = ReferenceFilter::LazyReference.load(refs) + result[:references][type].concat(values) end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 2e546ef0d5..8100f2675a 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -10,9 +10,10 @@ module Gitlab @current_user = current_user end - def analyze(text) + def analyze(texts) references.clear - @text = Gitlab::Markdown.render_without_gfm(text) + texts = Array(texts) + @texts = texts.map { |text| Gitlab::Markdown.render_without_gfm(text) } end %i(user label issue merge_request snippet commit commit_range).each do |type| @@ -47,13 +48,25 @@ module Gitlab current_user: current_user, # We don't actually care about the links generated only_path: true, - ignore_blockquotes: true + ignore_blockquotes: true, + load_lazy_references: false } pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) - result = pipeline.call(@text) - result[:references][filter_type] + values = [] + lazy_references = [] + + @texts.each do |text| + result = pipeline.call(text) + + values.concat(result[:references][filter_type]) + lazy_references.concat(result[:lazy_references][filter_type]) + end + + lazy_values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(lazy_references) + values.concat(lazy_values) + values end end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 088e34f050..ad84d2274e 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::ReferenceExtractor do project.team << [@u_bar, :guest] subject.analyze('@foo, @baduser, @bar, and @offteam') - expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) + expect(subject.users).to match_array([@u_foo, @u_bar, @u_offteam]) end it 'ignores user mentions inside specific elements' do @@ -37,7 +37,7 @@ describe Gitlab::ReferenceExtractor do > @offteam }) - expect(subject.users).to eq([]) + expect(subject.users).to match_array([]) end it 'accesses valid issue objects' do @@ -45,7 +45,7 @@ describe Gitlab::ReferenceExtractor do @i1 = create(:issue, project: project) subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") - expect(subject.issues).to eq([@i0, @i1]) + expect(subject.issues).to match_array([@i0, @i1]) end it 'accesses valid merge requests' do @@ -53,7 +53,7 @@ describe Gitlab::ReferenceExtractor do @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict') subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") - expect(subject.merge_requests).to eq([@m1, @m0]) + expect(subject.merge_requests).to match_array([@m1, @m0]) end it 'accesses valid labels' do @@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do @l2 = create(:label) subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") - expect(subject.labels).to eq([@l0, @l1]) + expect(subject.labels).to match_array([@l0, @l1]) end it 'accesses valid snippets' do @@ -71,7 +71,7 @@ describe Gitlab::ReferenceExtractor do @s2 = create(:project_snippet) subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") - expect(subject.snippets).to eq([@s0, @s1]) + expect(subject.snippets).to match_array([@s0, @s1]) end it 'accesses valid commits' do @@ -109,7 +109,7 @@ describe Gitlab::ReferenceExtractor do subject.analyze("this refers issue #{issue.to_reference(project)}") extracted = subject.issues expect(extracted.size).to eq(1) - expect(extracted).to eq([issue]) + expect(extracted).to match_array([issue]) end end end From cd2583a3beed95a91eddf4e6f868507dcf499481 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 23:03:53 +0200 Subject: [PATCH 048/443] Code cleanup --- lib/gitlab/markdown/reference_filter.rb | 10 +++++++++- .../markdown/reference_gatherer_filter.rb | 18 +++++------------- lib/gitlab/reference_extractor.rb | 12 +++--------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index ede9f8865f..adaca78ba2 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -14,10 +14,18 @@ module Gitlab class ReferenceFilter < HTML::Pipeline::Filter LazyReference = Struct.new(:klass, :ids) do def self.load(refs) - refs.group_by(&:klass).flat_map do |klass, refs| + lazy_references, values = refs.partition { |ref| ref.is_a?(self) } + + lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs| ids = refs.flat_map(&:ids) klass.where(id: ids) end + + values + lazy_values + end + + def load + self.klass.where(id: self.ids) end end diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index 18df5db94d..31fb71a98a 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -12,8 +12,7 @@ module Gitlab def initialize(*) super - result[:lazy_references] ||= Hash.new { |hash, type| hash[type] = [] } - result[:references] ||= Hash.new { |hash, type| hash[type] = [] } + result[:references] ||= Hash.new { |hash, type| hash[type] = [] } end def call @@ -41,23 +40,16 @@ module Gitlab references.each do |type, values| Array.wrap(values).each do |value| - refs = - if value.is_a?(ReferenceFilter::LazyReference) - result[:lazy_references] - else - result[:references] - end - - refs[type] << value + result[:references][type] << value end end end # Will load all references of one type using one query. def load_lazy_references - result[:lazy_references].each do |type, refs| - values = ReferenceFilter::LazyReference.load(refs) - result[:references][type].concat(values) + refs = result[:references] + refs.each do |type, values| + refs[type] = ReferenceFilter::LazyReference.load(values) end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 8100f2675a..d6b739d7b9 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -54,19 +54,13 @@ module Gitlab pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) - values = [] - lazy_references = [] - - @texts.each do |text| + values = @texts.flat_map do |text| result = pipeline.call(text) - values.concat(result[:references][filter_type]) - lazy_references.concat(result[:lazy_references][filter_type]) + result[:references][filter_type] end - lazy_values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(lazy_references) - values.concat(lazy_values) - values + Gitlab::Markdown::ReferenceFilter::LazyReference.load(values) end end end From 530f0d71f5d8157df3a469900e2fe0a81c9eaa5a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 01:07:58 -0400 Subject: [PATCH 049/443] Shut up, Rubocop --- spec/services/git_push_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 93bac384a6..fd72905c33 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -272,4 +272,4 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, new_ref) end end -end \ No newline at end of file +end From 0fbb544c502a30c751a4a8c8f954f853aece93b2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 02:39:59 -0400 Subject: [PATCH 050/443] Update uglifier to ~> 2.7.2 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 967092994a..5b6d4d8c08 100644 --- a/Gemfile +++ b/Gemfile @@ -196,7 +196,7 @@ gem 'charlock_holmes', '~> 0.6.9.4' gem "sass-rails", '~> 4.0.5' gem "coffee-rails", '~> 4.1.0' -gem "uglifier", '~> 2.3.2' +gem "uglifier", '~> 2.7.2' gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks', '~> 2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 58426a6068..6fd4d99598 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -741,7 +741,7 @@ GEM simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.3.3) + uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) underscore-rails (1.4.4) @@ -926,7 +926,7 @@ DEPENDENCIES thin (~> 1.6.1) tinder (~> 1.10.0) turbolinks (~> 2.5.0) - uglifier (~> 2.3.2) + uglifier (~> 2.7.2) underscore-rails (~> 1.4.4) unf (~> 0.1.4) unicorn (~> 4.8.2) From d6fb96b9276d1a9edfae261d2eba2f79f8a9f340 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 09:17:05 +0200 Subject: [PATCH 051/443] Have Issue#participants load all users mentioned in notes using a single query --- app/models/concerns/mentionable.rb | 8 ++++---- app/models/concerns/participable.rb | 23 +++++++++++++---------- lib/gitlab/reference_extractor.rb | 21 +++++++++++---------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 7ad8d5b7da..9339ecc4bc 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -47,19 +47,19 @@ module Mentionable SystemNoteService.cross_reference_exists?(target, local_reference) end - def mentioned_users(current_user = nil) + def mentioned_users(current_user = nil, load_lazy_references: true) return [] if mentionable_text.blank? - ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) ext.analyze(mentionable_text) ext.users.uniq end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def references(p = project, current_user = self.author, text = mentionable_text) + def references(p = project, current_user = self.author, text = mentionable_text, load_lazy_references: true) return [] if text.blank? - ext = Gitlab::ReferenceExtractor.new(p, current_user) + ext = Gitlab::ReferenceExtractor.new(p, current_user, load_lazy_references: load_lazy_references) ext.analyze(text) (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 7c9597333d..7a2bea567d 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -27,7 +27,7 @@ module Participable module ClassMethods def participant(*attrs) - participant_attrs.concat(attrs.map(&:to_s)) + participant_attrs.concat(attrs) end def participant_attrs @@ -37,13 +37,12 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author, project = self.project) + def participants(current_user = self.author, project = self.project, load_lazy_references: true) participants = self.class.participant_attrs.flat_map do |attr| meth = method(attr) - value = - if meth.arity == 1 || meth.arity == -1 - meth.call(current_user) + if attr == :mentioned_users + meth.call(current_user, load_lazy_references: false) else meth.call end @@ -51,9 +50,13 @@ module Participable participants_for(value, current_user, project) end.compact.uniq - if project - participants.select! do |user| - user.can?(:read_project, project) + if load_lazy_references + participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq + + if project + participants.select! do |user| + user.can?(:read_project, project) + end end end @@ -64,12 +67,12 @@ module Participable def participants_for(value, current_user = nil, project = nil) case value - when User + when User, Gitlab::Markdown::ReferenceFilter::LazyReference [value] when Enumerable, ActiveRecord::Relation value.flat_map { |v| participants_for(v, current_user, project) } when Participable - value.participants(current_user, project) + value.participants(current_user, project, load_lazy_references: false) end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index d6b739d7b9..895a23ddcc 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -3,17 +3,17 @@ require 'gitlab/markdown' module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :project, :current_user + attr_accessor :project, :current_user, :load_lazy_references - def initialize(project, current_user = nil) + def initialize(project, current_user = nil, load_lazy_references: true) @project = project @current_user = current_user + @load_lazy_references = load_lazy_references end - def analyze(texts) + def analyze(text) references.clear - texts = Array(texts) - @texts = texts.map { |text| Gitlab::Markdown.render_without_gfm(text) } + @text = Gitlab::Markdown.render_without_gfm(text) end %i(user label issue merge_request snippet commit commit_range).each do |type| @@ -29,7 +29,7 @@ module Gitlab type = type.to_sym return references[type] if references.has_key?(type) - references[type] = pipeline_result(type).uniq + references[type] = pipeline_result(type) end end @@ -53,14 +53,15 @@ module Gitlab } pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) + result = pipeline.call(@text) - values = @texts.flat_map do |text| - result = pipeline.call(text) + values = result[:references][filter_type].uniq - result[:references][filter_type] + if @load_lazy_references + values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq end - Gitlab::Markdown::ReferenceFilter::LazyReference.load(values) + values end end end From 613aa1cb87ee1000cb08bd15db1b62eae64f19dd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 07:21:15 +0000 Subject: [PATCH 052/443] Add defaults for incoming_email ssl and start_tls. --- config/initializers/1_settings.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index f04263c760..432bf026ba 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -187,9 +187,11 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci[ # Reply by email # Settings['incoming_email'] ||= Settingslogic.new({}) -Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? -Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil? -Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? +Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? +Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil? +Settings.incoming_email['ssl'] = 143 if Settings.incoming_email['ssl'].nil? +Settings.incoming_email['start_tls'] = 143 if Settings.incoming_email['start_tls'].nil? +Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? # # Gravatar @@ -267,4 +269,4 @@ if Rails.env.test? Settings.gitlab['default_projects_limit'] = 42 Settings.gitlab['default_can_create_group'] = true Settings.gitlab['default_can_create_team'] = false -end +end \ No newline at end of file From 276b3a7bc202bd9b51c8f5401f4c525227f3a4d8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 09:27:30 +0200 Subject: [PATCH 053/443] Make Mentionable#cross_reference_exists? private. --- app/models/concerns/mentionable.rb | 12 ++++++------ spec/support/mentionable_shared_examples.rb | 7 ------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 5f53ea2563..b34def66d2 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -41,12 +41,6 @@ module Mentionable self end - # Determine whether or not a cross-reference Note has already been created between this Mentionable and - # the specified target. - def cross_reference_exists?(target) - SystemNoteService.cross_reference_exists?(target, local_reference) - end - def all_references(current_user = self.author, text = self.mentionable_text) ext = Gitlab::ReferenceExtractor.new(self.project, current_user) ext.analyze(text) @@ -111,4 +105,10 @@ module Mentionable # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } end + + # Determine whether or not a cross-reference Note has already been created between this Mentionable and + # the specified target. + def cross_reference_exists?(target) + SystemNoteService.cross_reference_exists?(target, local_reference) + end end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 412c6f4ead..f584904845 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -86,13 +86,6 @@ shared_examples 'a mentionable' do subject.create_cross_references! end - - it 'detects existing cross-references' do - SystemNoteService.cross_reference(mentioned_issue, subject.local_reference, author) - - expect(subject.cross_reference_exists?(mentioned_issue)).to be_truthy - expect(subject.cross_reference_exists?(mentioned_mr)).to be_falsey - end end shared_examples 'an editable mentionable' do From 7f7d39858f87d5920137d63e5d6005a0989ca392 Mon Sep 17 00:00:00 2001 From: Iman Mohamadi Date: Wed, 14 Oct 2015 11:14:59 +0330 Subject: [PATCH 054/443] ugly outlines removed form sidebar --- app/assets/stylesheets/framework/sidebar.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c5ea3aca7c..1c42ba2fd7 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -64,6 +64,7 @@ text-decoration: none; padding-left: 22px; font-weight: normal; + outline: none; &:hover { text-decoration: none; @@ -176,6 +177,7 @@ text-align: center; line-height: 40px; transition-duration: .3s; + outline: none; } .collapse-nav a:hover { @@ -238,6 +240,7 @@ width: 100%; padding: 10px 22px; overflow: hidden; + outline: none; img { width: 36px; From d313a7681be1f307d8178eca13a2e73118f80930 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 09:46:06 +0200 Subject: [PATCH 055/443] Explicitly only parse references by specified filter --- lib/gitlab/markdown/reference_gatherer_filter.rb | 2 ++ lib/gitlab/reference_extractor.rb | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb index 31fb71a98a..00f983675e 100644 --- a/lib/gitlab/markdown/reference_gatherer_filter.rb +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -33,6 +33,8 @@ module Gitlab reference_type = node.attr('data-reference-filter') reference_filter = reference_type.constantize + return if context[:reference_filter] && reference_filter != context[:reference_filter] + return unless reference_filter.user_can_reference?(current_user, node, context) references = reference_filter.referenced_by(node) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 895a23ddcc..66016ecc87 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -40,16 +40,20 @@ module Gitlab # # Returns the results Array for the requested filter type def pipeline_result(filter_type) - klass = filter_type.to_s.camelize + 'ReferenceFilter' + klass = "#{filter_type.to_s.camelize}ReferenceFilter" filter = Gitlab::Markdown.const_get(klass) context = { project: project, current_user: current_user, + # We don't actually care about the links generated only_path: true, ignore_blockquotes: true, - load_lazy_references: false + + # ReferenceGathererFilter + load_lazy_references: false, + reference_filter: filter } pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) From de026375fc4eea39ae4bccb2de3bbed93de8c86b Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Wed, 14 Oct 2015 19:41:12 +1100 Subject: [PATCH 056/443] Updated the style of the snippets header in #show It should now more closly match the styles used in issues and merge requests with some small tweaks to be more relevant to snippets --- app/assets/stylesheets/pages/snippets.scss | 55 ++++++++++++++++++++++ app/views/snippets/show.html.haml | 51 +++++++++----------- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index a3d7aba054..39034a1391 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -30,3 +30,58 @@ } } } + +.snippet-details { + .page-title { + margin-top: -15px; + padding: 10px 0; + margin-bottom: 0; + color: #5c5d5e; + font-size: 16px; + + .author { + color: #5c5d5e; + } + + .snippet-id { + color: #5c5d5e; + } + .btn { + padding: 10px $gl-padding; + } + } + + .snippet-title { + margin: 0; + font-size: 23px; + color: #313236; + } + + @media (max-width: $screen-md-max) { + .new-snippet-link { + display: none; + } + } + + @media (max-width: $screen-sm-max) { + .creator, + .page-title .btn-close { + display: none; + } + } +} + +.snippet-box { + @include border-radius(2px); + + display: inline-block; + padding: 10px $gl-padding; + font-weight: normal; + margin-right: 10px; + font-size: $gl-font-size; + + &.snippet-box-locked { + background: $gl-gray; + color: #FFF; + } +} diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index a12cfd0ff4..f2519abea6 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,30 +1,29 @@ - page_title @snippet.title, "Snippets" -%h4.page-title - = @snippet.title - - if @snippet.private? - %span.label.label-success - %i.fa.fa-lock - private +.snippet + .snippet-details + .page-title + - if @snippet.private? + .snippet-box.snippet-box-locked + %i.fa.fa-lock + Private + %span.creator + updated by #{link_to_member(@project, @snippet.author, size: 24)} + · + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_updated_ago') - .pull-right - = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do - Add new snippet - -.append-bottom-10.prepend-top-10.clearfix - .pull-right - %span.light - = link_to user_snippets_path(@snippet.author) do - = @snippet.author_name - authored #{time_ago_with_tooltip(@snippet.updated_at)} - - .back-link - - if @snippet.author == current_user - = link_to dashboard_snippets_path do - ← your snippets - - else - = link_to explore_snippets_path do - ← explore snippets + .pull-right + = link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do + = icon('plus') + Add new snippet + = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' + - if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped issuabled-edit" do + = icon('pencil-square-o') + Edit + .gray-content-block.middle-block + %h2.snippet-title + = gfm escape_once(@snippet.title) .file-holder .file-title @@ -33,9 +32,5 @@ = @snippet.file_name .file-actions .btn-group - - if can?(current_user, :update_personal_snippet, @snippet) - = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet' = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' From 033a879cc96c6210175f03f72547d05a2946c0bb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 14 Oct 2015 11:12:21 +0200 Subject: [PATCH 057/443] Fix NGINX API download regex Users are allowed to supply namespace%2Fproject instead of a numeric ID --- lib/support/nginx/gitlab | 2 +- lib/support/nginx/gitlab-ssl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index ffc0eb0585..1e55c5a048 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -125,7 +125,7 @@ server { return 418; } - location ~ ^/api/v3/projects/[0-9]+/repository/archive { + location ~ ^/api/v3/projects/.*/repository/archive { # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block error_page 418 = @gitlab-git-http-server; return 418; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index c2e9f8864f..08641bbcc1 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -172,7 +172,7 @@ server { return 418; } - location ~ ^/api/v3/projects/[0-9]+/repository/archive { + location ~ ^/api/v3/projects/.*/repository/archive { # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block error_page 418 = @gitlab-git-http-server; return 418; From 0bea5ced8bf4c9306f8f8e912313731a43d16f4c Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Wed, 14 Oct 2015 09:04:22 +1100 Subject: [PATCH 058/443] Made suggested content changes based on MR Review Changed the authentication method for removing fork through API Reflected changes to new auth method in API specs --- CHANGELOG | 2 +- app/controllers/projects_controller.rb | 6 ++- app/views/projects/edit.html.haml | 8 ++-- config/routes.rb | 2 +- lib/api/projects.rb | 2 +- spec/controllers/projects_controller_spec.rb | 26 ++++++---- spec/features/projects_spec.rb | 6 +-- spec/requests/api/projects_spec.rb | 50 +++++++++++++------- 8 files changed, 63 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cf9e5b43c4..e45fd2ef5a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,7 +46,7 @@ v 8.1.0 (unreleased) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) - Adds ability to remove the forked relationship from project settings - screen. #2578 (Han Loong Liauw) + screen. (Han Loong Liauw) - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1fb83d0eb6..77b3af9a5d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController before_action :repository, except: [:new, :create] # Authorize - before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive, :remove_fork] + before_action :authorize_admin_project!, only: [:edit, :update] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -56,6 +56,8 @@ class ProjectsController < ApplicationController end def transfer + return access_denied! unless can?(current_user, :change_namespace, @project) + namespace = Namespace.find_by(id: params[:new_namespace_id]) ::Projects::TransferService.new(project, current_user).execute(namespace) @@ -65,6 +67,8 @@ class ProjectsController < ApplicationController end def remove_fork + return access_denied! unless can?(current_user, :remove_fork_project, @project) + if @project.forked? @project.forked_project_link.destroy flash[:notice] = 'Fork relationship has been removed.' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ce77a9242f..ec58d0924b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -190,17 +190,17 @@ .nothing-here-block Only the project owner can transfer a project - if @project.forked? && can?(current_user, :remove_fork_project, @project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| .panel.panel-default.panel.panel-danger - .panel-heading Remove forked relationship + .panel-heading Remove fork relationship .panel-body %p This will remove the relationship to the source project from = link_to project_path(@project.forked_from_project) do = @project.forked_from_project.namespace.try(:name) %br - %strong Once removed it cannot be reversed through this interface - = button_to 'Remove forked relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + %strong Once removed it cannot be reversed through this interface. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - elsif @project.forked? .nothing-here-block Only the project owner can remove the fork relationship diff --git a/config/routes.rb b/config/routes.rb index 3ac4342b23..64bdd189f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -378,7 +378,7 @@ Gitlab::Application.routes.draw do [:new, :create, :index], path: "/") do member do put :transfer - put :remove_fork + delete :remove_fork post :archive post :unarchive post :toggle_star diff --git a/lib/api/projects.rb b/lib/api/projects.rb index e8a0e7f3ec..67ee66a205 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -246,7 +246,7 @@ module API # Example Request: # DELETE /projects/:id/fork delete ":id/fork" do - authenticated_as_admin! + authorize! :remove_fork_project, user_project if user_project.forked? user_project.forked_project_link.destroy end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 626b56cc78..e963e91351 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -72,9 +72,12 @@ describe ProjectsController do context 'with forked project' do let(:project_fork) { create(:project, namespace: user.namespace) } - it 'should remove fork from project' do + before do create(:forked_project_link, forked_to_project: project_fork) - put(:remove_fork, + end + + it 'should remove fork from project' do + delete(:remove_fork, namespace_id: project_fork.namespace.to_param, id: project_fork.to_param, format: :js) @@ -84,19 +87,22 @@ describe ProjectsController do end end - it 'should do nothing if project was not forked' do - unforked_project = create(:project, namespace: user.namespace) - put(:remove_fork, - namespace_id: unforked_project.namespace.to_param, - id: unforked_project.to_param, format: :js) + context 'when project not forked' do + let(:unforked_project) { create(:project, namespace: user.namespace) } - expect(flash[:notice]).to be_nil - expect(response).to render_template(:remove_fork) + it 'should do nothing if project was not forked' do + delete(:remove_fork, + namespace_id: unforked_project.namespace.to_param, + id: unforked_project.to_param, format: :js) + + expect(flash[:notice]).to be_nil + expect(response).to render_template(:remove_fork) + end end end it "does nothing if user is not signed in" do - put(:remove_fork, + delete(:remove_fork, namespace_id: project.namespace.to_param, id: project.to_param, format: :js) expect(response.status).to eq(401) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index df0dcb2bb2..f3d51641ec 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -45,13 +45,13 @@ feature 'Project', feature: true do end it 'should remove fork' do - expect(page).to have_content 'Remove forked relationship' + expect(page).to have_content 'Remove fork relationship' - remove_with_confirm('Remove forked relationship', project.path) + remove_with_confirm('Remove fork relationship', project.path) expect(page).to have_content 'Fork relationship has been removed.' expect(project.forked?).to be_falsey - expect(page).not_to have_content 'Remove forked relationship' + expect(page).not_to have_content 'Remove fork relationship' end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 580bbec77d..e9de9e0826 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -606,28 +606,42 @@ describe API::API, api: true do describe 'DELETE /projects/:id/fork' do - it "shouldn't available for non admin users" do + it "shouldn't be visible to users outside group" do delete api("/projects/#{project_fork_target.id}/fork", user) - expect(response.status).to eq(403) + expect(response.status).to eq(404) end - it 'should make forked project unforked' do - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - project_fork_target.reload - expect(project_fork_target.forked_from_project).not_to be_nil - expect(project_fork_target.forked?).to be_truthy - delete api("/projects/#{project_fork_target.id}/fork", admin) - expect(response.status).to eq(200) - project_fork_target.reload - expect(project_fork_target.forked_from_project).to be_nil - expect(project_fork_target.forked?).not_to be_truthy - end + context 'when users belong to project group' do + let(:project_fork_target) { create(:project, group: create(:group)) } - it 'should be idempotent if not forked' do - expect(project_fork_target.forked_from_project).to be_nil - delete api("/projects/#{project_fork_target.id}/fork", admin) - expect(response.status).to eq(200) - expect(project_fork_target.reload.forked_from_project).to be_nil + before do + project_fork_target.group.add_owner user + project_fork_target.group.add_developer user2 + end + + it 'should be forbidden to non-owner users' do + delete api("/projects/#{project_fork_target.id}/fork", user2) + expect(response.status).to eq(403) + end + + it 'should make forked project unforked' do + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + expect(project_fork_target.forked_from_project).not_to be_nil + expect(project_fork_target.forked?).to be_truthy + delete api("/projects/#{project_fork_target.id}/fork", admin) + expect(response.status).to eq(200) + project_fork_target.reload + expect(project_fork_target.forked_from_project).to be_nil + expect(project_fork_target.forked?).not_to be_truthy + end + + it 'should be idempotent if not forked' do + expect(project_fork_target.forked_from_project).to be_nil + delete api("/projects/#{project_fork_target.id}/fork", admin) + expect(response.status).to eq(200) + expect(project_fork_target.reload.forked_from_project).to be_nil + end end end end From a60853fda09b37c2990b2437baa7a5d13b54572d Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Wed, 14 Oct 2015 20:37:36 +1100 Subject: [PATCH 059/443] include created_at date in heading --- app/views/snippets/show.html.haml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index f2519abea6..9d20b76cbf 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,15 +7,21 @@ .snippet-box.snippet-box-locked %i.fa.fa-lock Private + %span.snippet-id Snippet ##{@snippet.id} %span.creator - updated by #{link_to_member(@project, @snippet.author, size: 24)} + · created by #{link_to_member(@project, @snippet.author, size: 24)} · - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_updated_ago') + = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + - if @snippet.updated_at != @snippet.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') .pull-right = link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do = icon('plus') - Add new snippet + new snippet = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped issuabled-edit" do From fc94b3b036c565753b4ae9740ddeeaa40525758b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 14 Oct 2015 12:13:17 +0200 Subject: [PATCH 060/443] Fix API archive specs --- spec/requests/api/repositories_spec.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 09a79553f7..1149f7e798 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -166,24 +166,21 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/archive", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/) - expect(response.content_type).to eq(MIME::Types.type_for('file.tar.gz').first.content_type) + expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) end it "should get the archive.zip" do get api("/projects/#{project.id}/repository/archive.zip", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.zip\"/) - expect(response.content_type).to eq(MIME::Types.type_for('file.zip').first.content_type) + expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) end it "should get the archive.tar.bz2" do get api("/projects/#{project.id}/repository/archive.tar.bz2", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/) - expect(response.content_type).to eq(MIME::Types.type_for('file.tar.bz2').first.content_type) + expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) end it "should return 404 for invalid sha" do From 8f14332625d2031cb8275d1a4c8293120a25538d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 14 Oct 2015 12:17:59 +0200 Subject: [PATCH 061/443] Remove RepositoryArchiveWorker specs These tasks have shifted to gitlab_git and gitlab-git-http-server. --- .../archive_repository_service_spec.rb | 67 +--------------- .../workers/repository_archive_worker_spec.rb | 79 ------------------- 2 files changed, 1 insertion(+), 145 deletions(-) delete mode 100644 spec/workers/repository_archive_worker_spec.rb diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb index 0ec70c51b3..1cc7b24021 100644 --- a/spec/services/archive_repository_service_spec.rb +++ b/spec/services/archive_repository_service_spec.rb @@ -13,7 +13,7 @@ describe ArchiveRepositoryService do context "when the repository doesn't have an archive file path" do before do - allow(project.repository).to receive(:archive_file_path).and_return(nil) + allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) end it "raises an error" do @@ -21,70 +21,5 @@ describe ArchiveRepositoryService do end end - context "when the repository has an archive file path" do - let(:file_path) { "/archive.zip" } - let(:pid_file_path) { "/archive.zip.pid" } - - before do - allow(project.repository).to receive(:archive_file_path).and_return(file_path) - allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) - end - - context "when the archive file already exists" do - before do - allow(File).to receive(:exist?).with(file_path).and_return(true) - end - - it "returns the file path" do - expect(subject.execute(timeout: 0.0)).to eq(file_path) - end - end - - context "when the archive file doesn't exist yet" do - before do - allow(File).to receive(:exist?).with(file_path).and_return(false) - allow(File).to receive(:exist?).with(pid_file_path).and_return(true) - end - - context "when the archive pid file doesn't exist yet" do - before do - allow(File).to receive(:exist?).with(pid_file_path).and_return(false) - end - - it "queues the RepositoryArchiveWorker" do - expect(RepositoryArchiveWorker).to receive(:perform_async) - - subject.execute(timeout: 0.0) - end - end - - context "when the archive pid file already exists" do - it "doesn't queue the RepositoryArchiveWorker" do - expect(RepositoryArchiveWorker).not_to receive(:perform_async) - - subject.execute(timeout: 0.0) - end - end - - context "when the archive file exists after a little while" do - before do - Thread.new do - sleep 0.1 - allow(File).to receive(:exist?).with(file_path).and_return(true) - end - end - - it "returns the file path" do - expect(subject.execute(timeout: 0.2)).to eq(file_path) - end - end - - context "when the archive file doesn't exist after the timeout" do - it "returns nil" do - expect(subject.execute(timeout: 0.0)).to eq(nil) - end - end - end - end end end diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb deleted file mode 100644 index a914d0ac8d..0000000000 --- a/spec/workers/repository_archive_worker_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'spec_helper' - -describe RepositoryArchiveWorker do - let(:project) { create(:project) } - subject { RepositoryArchiveWorker.new } - - before do - allow(Project).to receive(:find).and_return(project) - end - - describe "#perform" do - it "cleans old archives" do - expect(project.repository).to receive(:clean_old_archives) - - subject.perform(project.id, "master", "zip") - end - - context "when the repository doesn't have an archive file path" do - before do - allow(project.repository).to receive(:archive_file_path).and_return(nil) - end - - it "doesn't archive the repo" do - expect(project.repository).not_to receive(:archive_repo) - - subject.perform(project.id, "master", "zip") - end - end - - context "when the repository has an archive file path" do - let(:file_path) { "/archive.zip" } - let(:pid_file_path) { "/archive.zip.pid" } - - before do - allow(project.repository).to receive(:archive_file_path).and_return(file_path) - allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) - end - - context "when the archive file already exists" do - before do - allow(File).to receive(:exist?).with(file_path).and_return(true) - end - - it "doesn't archive the repo" do - expect(project.repository).not_to receive(:archive_repo) - - subject.perform(project.id, "master", "zip") - end - end - - context "when the archive file doesn't exist yet" do - before do - allow(File).to receive(:exist?).with(file_path).and_return(false) - allow(File).to receive(:exist?).with(pid_file_path).and_return(true) - end - - context "when the archive pid file doesn't exist yet" do - before do - allow(File).to receive(:exist?).with(pid_file_path).and_return(false) - end - - it "archives the repo" do - expect(project.repository).to receive(:archive_repo) - - subject.perform(project.id, "master", "zip") - end - end - - context "when the archive pid file already exists" do - it "doesn't archive the repo" do - expect(project.repository).not_to receive(:archive_repo) - - subject.perform(project.id, "master", "zip") - end - end - end - end - end -end From 3a69dd20a1d8e54c25f871f35f09a1b8b5d4b2a4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 12:33:39 +0200 Subject: [PATCH 062/443] Allow dashboard and group issues/MRs to be filtered by label --- CHANGELOG | 1 + app/helpers/labels_helper.rb | 18 ++++++++++---- app/models/group_label.rb | 9 +++++++ app/models/group_milestone.rb | 12 ++-------- app/services/labels/group_service.rb | 26 +++++++++++++++++++++ app/views/shared/issuable/_filter.html.haml | 9 ++++--- 6 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 app/models/group_label.rb create mode 100644 app/services/labels/group_service.rb diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..0a0afb4a05 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) + - Allow dashboard and group issues/MRs to be filtered by label v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 66b18eea69..ee04ace35d 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -92,11 +92,19 @@ module LabelsHelper end end - def project_labels_options(project) - labels = project.labels.to_a - labels.unshift(Label::None) - labels.unshift(Label::Any) - options_from_collection_for_select(labels, 'name', 'title', params[:label_name]) + def projects_labels_options + labels = + if @project + @project.labels + else + Label.where(project_id: @projects) + end + + grouped_labels = Labels::GroupService.new(labels).execute + grouped_labels.unshift(Label::None) + grouped_labels.unshift(Label::Any) + + options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) end # Required for Gitlab::Markdown::LabelReferenceFilter diff --git a/app/models/group_label.rb b/app/models/group_label.rb new file mode 100644 index 0000000000..0fc39cb877 --- /dev/null +++ b/app/models/group_label.rb @@ -0,0 +1,9 @@ +class GroupLabel + attr_accessor :title, :labels + alias_attribute :name, :title + + def initialize(title, labels) + @title = title + @labels = labels + end +end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 1dd2be68eb..91844da62e 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -1,5 +1,5 @@ class GroupMilestone - + attr_accessor :title, :milestones alias_attribute :name, :title def initialize(title, milestones) @@ -7,18 +7,10 @@ class GroupMilestone @milestones = milestones end - def title - @title - end - def safe_title @title.parameterize end - - def milestones - @milestones - end - + def projects milestones.map { |milestone| milestone.project } end diff --git a/app/services/labels/group_service.rb b/app/services/labels/group_service.rb new file mode 100644 index 0000000000..b26cee24d5 --- /dev/null +++ b/app/services/labels/group_service.rb @@ -0,0 +1,26 @@ +module Labels + class GroupService < ::BaseService + def initialize(project_labels) + @project_labels = project_labels.group_by(&:title) + end + + def execute + build(@project_labels) + end + + def label(title) + if title + group_label = @project_labels[title].group_by(&:title) + build(group_label).first + else + nil + end + end + + private + + def build(label) + label.map { |title, labels| GroupLabel.new(title, labels) } + end + end +end diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 8f16773077..0e4e9c0987 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -42,11 +42,10 @@ class: 'select2 trigger-submit', include_blank: true, data: {placeholder: 'Milestone'}) - - if @project - .filter-item.inline.labels-filter - = select_tag('label_name', project_labels_options(@project), - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Label'}) + .filter-item.inline.labels-filter + = select_tag('label_name', projects_labels_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Label'}) .pull-right = render 'shared/sort_dropdown' From 61d8f9617681973f04d264762b54840d44dea02a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 13:46:17 +0200 Subject: [PATCH 063/443] Fix specs --- spec/lib/gitlab/closing_issue_extractor_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 5d7ff4f612..21254f778d 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -140,28 +140,28 @@ describe Gitlab::ClosingIssueExtractor do message = "Closes #{reference} and fix #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do message = "Closes #{reference}, closes #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do message = "Closes #{reference}, #{reference2} and #{reference3}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue, third_issue]) + to match_array([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches issues in hybrid message' do @@ -169,7 +169,7 @@ describe Gitlab::ClosingIssueExtractor do "Also fixing issues #{reference2}, #{reference3} and #4" expect(subject.closed_by_message(message)). - to eq([issue, other_issue, third_issue]) + to match_array([issue, other_issue, third_issue]) end end end From 1f92c22fec8493fa0efb8a53829a4c726e78934e Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Wed, 14 Oct 2015 23:12:08 +1100 Subject: [PATCH 064/443] New snippet design for projects Split out header into shared partial Used action partials to have unique actions for shared and personal snippets changed back to created date in list view Switched to using existing color classes --- app/assets/stylesheets/pages/snippets.scss | 16 +++++----- .../projects/snippets/_actions.html.haml | 9 ++++++ app/views/projects/snippets/show.html.haml | 28 +--------------- app/views/shared/snippets/_header.html.haml | 25 +++++++++++++++ app/views/shared/snippets/_snippet.html.haml | 2 +- app/views/snippets/_actions.html.haml | 8 +++++ app/views/snippets/show.html.haml | 32 +------------------ 7 files changed, 53 insertions(+), 67 deletions(-) create mode 100644 app/views/projects/snippets/_actions.html.haml create mode 100644 app/views/shared/snippets/_header.html.haml create mode 100644 app/views/snippets/_actions.html.haml diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 39034a1391..af39148176 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -37,10 +37,14 @@ padding: 10px 0; margin-bottom: 0; color: #5c5d5e; - font-size: 16px; + font-size: 13px; + @include clearfix(); - .author { - color: #5c5d5e; + .creator { + color: $gl-gray; + a { + color: $gl-gray; + } } .snippet-id { @@ -79,9 +83,5 @@ font-weight: normal; margin-right: 10px; font-size: $gl-font-size; - - &.snippet-box-locked { - background: $gl-gray; - color: #FFF; - } + border: 1px solid; } diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml new file mode 100644 index 0000000000..4d5f702637 --- /dev/null +++ b/app/views/projects/snippets/_actions.html.haml @@ -0,0 +1,9 @@ += link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do + = icon('plus') + new snippet +- if can?(current_user, :admin_project_snippet, @snippet) + = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' +- if can?(current_user, :update_project_snippet, @snippet) + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + = icon('pencil-square-o') + Edit diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index be7d4d486f..4f236fcc7e 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,27 +1,6 @@ - page_title @snippet.title, "Snippets" = render "header_title" - -%h3.page-title - = @snippet.title - - .pull-right - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do - Add new snippet - -%hr - -.append-bottom-20 - .pull-right - = "##{@snippet.id}" - %span.light - by - = link_to user_path(@snippet.author) do - = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" - = @snippet.author_name - - .back-link - = link_to namespace_project_snippets_path(@project.namespace, @project) do - ← project snippets += render 'shared/snippets/header' .file-holder .file-title @@ -30,11 +9,6 @@ = @snippet.file_name .file-actions .btn-group - - if can?(current_user, :update_project_snippet, @snippet) - = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet' = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' - = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml new file mode 100644 index 0000000000..95786ee337 --- /dev/null +++ b/app/views/shared/snippets/_header.html.haml @@ -0,0 +1,25 @@ +.snippet + .snippet-details + .page-title + .snippet-box{class: visibility_level_color(@snippet.visibility_level)} + = visibility_level_icon(@snippet.visibility_level) + = visibility_level_label(@snippet.visibility_level) + %span.snippet-id Snippet ##{@snippet.id} + %span.creator + · created by #{link_to_member(@project, @snippet.author, size: 24)} + · + = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + - if @snippet.updated_at != @snippet.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') + + .pull-right + - if @snippet.project_id? + = render "projects/snippets/actions" + - else + = render "snippets/actions" + .gray-content-block.middle-block + %h2.snippet-title + = gfm escape_once(@snippet.title) diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 6f4b0453ad..c6294caddc 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -17,4 +17,4 @@ = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: '' = snippet.author_name - authored #{time_ago_with_tooltip(snippet.updated_at)} + authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml new file mode 100644 index 0000000000..a4fdf89a4e --- /dev/null +++ b/app/views/snippets/_actions.html.haml @@ -0,0 +1,8 @@ += link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do + = icon('plus') + new snippet += link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' +- if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = icon('pencil-square-o') + Edit diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 9d20b76cbf..202a43bc57 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,35 +1,5 @@ - page_title @snippet.title, "Snippets" - -.snippet - .snippet-details - .page-title - - if @snippet.private? - .snippet-box.snippet-box-locked - %i.fa.fa-lock - Private - %span.snippet-id Snippet ##{@snippet.id} - %span.creator - · created by #{link_to_member(@project, @snippet.author, size: 24)} - · - = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') - - .pull-right - = link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do - = icon('plus') - new snippet - = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' - - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped issuabled-edit" do - = icon('pencil-square-o') - Edit - .gray-content-block.middle-block - %h2.snippet-title - = gfm escape_once(@snippet.title) += render 'shared/snippets/header' .file-holder .file-title From 381ca79bfa59d8d78a8bb1a7ee60cb46a87e4929 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 14 Oct 2015 15:21:14 +0200 Subject: [PATCH 065/443] Remove archive file sending spec This is done by gitlab-git-http-server now. --- .../projects/repositories_controller_spec.rb | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 91856ed0cc..18a30033ed 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -33,33 +33,5 @@ describe Projects::RepositoriesController do expect(response.status).to eq(404) end end - - context "when the service doesn't return a path" do - - before do - allow(service).to receive(:execute).and_return(nil) - end - - it "reloads the page" do - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" - - expect(response).to redirect_to(archive_namespace_project_repository_path(project.namespace, project, ref: "master", format: "zip")) - end - end - - context "when the service returns a path" do - - let(:path) { Rails.root.join("spec/fixtures/dk.png").to_s } - - before do - allow(service).to receive(:execute).and_return(path) - end - - it "sends the file" do - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" - - expect(response.body).to eq(File.binread(path)) - end - end end end From a74915a4adb4bc116f039dfb2438ae97ffde4e7e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 14 Oct 2015 15:22:03 +0200 Subject: [PATCH 066/443] Always return HTML in git_not_found This allows us to give a nice 404 for e.g. archive.zip. --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 527c9da0fa..be217e121b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -154,7 +154,7 @@ class ApplicationController < ActionController::Base end def git_not_found! - render "errors/git_not_found", layout: "errors", status: 404 + render html: "errors/git_not_found", layout: "errors", status: 404 end def method_missing(method_sym, *arguments, &block) From b46397548056e4e8ef00efe4f641c61ba1dd5230 Mon Sep 17 00:00:00 2001 From: Alex Lossent Date: Tue, 13 Oct 2015 11:29:57 +0200 Subject: [PATCH 067/443] Improve invalidation of stored service password if the endpoint URL is changed It now allows to specify a password at the same time as the new URL, and works on the service template admin pages. --- app/controllers/admin/services_controller.rb | 8 +++- .../projects/services_controller.rb | 8 +++- app/models/project_services/bamboo_service.rb | 2 +- .../project_services/teamcity_service.rb | 2 +- app/models/service.rb | 32 ++++++++++++--- spec/models/service_spec.rb | 39 +++++++++++++++++-- 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index a62170662e..4613358833 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -39,7 +39,13 @@ class Admin::ServicesController < Admin::ApplicationController end def application_services_params - params.permit(:id, + application_services_params = params.permit(:id, service: Projects::ServicesController::ALLOWED_PARAMS) + if application_services_params[:service].is_a?(Hash) + Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param| + application_services_params[:service].delete(param) if application_services_params[:service][param].blank? + end + end + application_services_params end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 3047ee8a1f..129068ef01 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,6 +9,10 @@ class Projects::ServicesController < Projects::ApplicationController :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] + + # Parameters to ignore if no value is specified + FILTER_BLANK_PARAMS = [:password] + # Authorize before_action :authorize_admin_project! before_action :service, only: [:edit, :update, :test] @@ -59,7 +63,9 @@ class Projects::ServicesController < Projects::ApplicationController def service_params service_params = params.require(:service).permit(ALLOWED_PARAMS) - service_params.delete("password") if service_params["password"].blank? + FILTER_BLANK_PARAMS.each do |param| + service_params.delete(param) if service_params[param].blank? + end service_params end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 5f5255ab48..4a18c772e5 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -48,7 +48,7 @@ class BambooService < CiService end def reset_password - if prop_updated?(:bamboo_url) + if prop_modified?(:bamboo_url) && !prop_updated?(:password) self.password = nil end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index fb11cad352..a548a557df 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -45,7 +45,7 @@ class TeamcityService < CiService end def reset_password - if prop_updated?(:teamcity_url) + if prop_modified?(:teamcity_url) && !prop_updated?(:password) self.password = nil end end diff --git a/app/models/service.rb b/app/models/service.rb index 7e845d565b..946ea1a096 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -33,6 +33,8 @@ class Service < ActiveRecord::Base after_initialize :initialize_properties + after_commit :reset_updated_properties + belongs_to :project has_one :service_hook @@ -103,6 +105,7 @@ class Service < ActiveRecord::Base # Provide convenient accessor methods # for each serialized property. + # Also keep track of updated properties. def self.prop_accessor(*args) args.each do |arg| class_eval %{ @@ -111,19 +114,36 @@ class Service < ActiveRecord::Base end def #{arg}=(value) + updated_properties['#{arg}'] = #{arg} unless updated_properties.include?('#{arg}') self.properties['#{arg}'] = value end } end end - # ActiveRecord does not provide a mechanism to track changes in serialized keys. - # This is why we need to perform extra query to do it mannually. + # Returns a hash of the properties that have been assigned a new value since last save, + # indicating their original values (attr => original value). + # ActiveRecord does not provide a mechanism to track changes in serialized keys, + # so we need a specific implementation for service properties. + # This allows to track changes to properties set with the accessor methods, + # but not direct manipulation of properties hash. + def updated_properties + @updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new + end + def prop_updated?(prop_name) - relation_name = self.type.underscore - previous_value = project.send(relation_name).send(prop_name) - return false if previous_value.nil? - previous_value != send(prop_name) + # Check if a new value was set. + # The new value may or may not be the same as the old value + updated_properties.include?(prop_name) + end + + def prop_modified?(prop_name) + # Check if new value was set and it is different from the old value + prop_updated?(prop_name) && updated_properties[prop_name] != send(prop_name) + end + + def reset_updated_properties + @updated_properties = nil end def async_execute(data) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index da87ea5b84..d42b96294b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -116,14 +116,47 @@ describe Service do ) end - it "returns false" do + it "returns false when the property has not been assigned a new value" do service.username = "key_changed" expect(service.prop_updated?(:bamboo_url)).to be_falsy end - it "returns true" do - service.bamboo_url = "http://other.com" + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.prop_updated?(:bamboo_url)).to be_truthy + end + + it "returns true when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' expect(service.prop_updated?(:bamboo_url)).to be_truthy end end + + describe "#prop_modified?" do + let(:service) do + BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.prop_modified?(:bamboo_url)).to be_falsy + end + + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.prop_modified?(:bamboo_url)).to be_truthy + end + + it "returns false when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.prop_modified?(:bamboo_url)).to be_falsy + end + end end From 4a5b77188ec7525d1c3a1ee925c8791f841b040c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 16:20:11 +0200 Subject: [PATCH 068/443] Participable doesn't need to know about Mentionable --- app/models/commit.rb | 4 ++-- app/models/concerns/issuable.rb | 4 ++-- app/models/concerns/mentionable.rb | 6 ++++++ app/models/concerns/participable.rb | 9 ++++----- app/models/note.rb | 4 ++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index aff329d71f..95ac7156be 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -2,13 +2,13 @@ class Commit extend ActiveModel::Naming include ActiveModel::Conversion - include Mentionable include Participable + include Mentionable include Referable include StaticModel attr_mentionable :safe_message - participant :author, :committer, :notes, :mentioned_users + participant :author, :committer, :notes attr_accessor :project diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 4db4ffb2e7..feee8460b8 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,8 +6,8 @@ # module Issuable extend ActiveSupport::Concern - include Mentionable include Participable + include Mentionable included do belongs_to :author, class_name: "User" @@ -47,7 +47,7 @@ module Issuable prefix: true attr_mentionable :title, :description - participant :author, :assignee, :notes, :mentioned_users + participant :author, :assignee, :notes end module ClassMethods diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 9339ecc4bc..715fc6f689 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,6 +20,12 @@ module Mentionable end end + included do + if self < Participable + participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) } + end + end + # Returns the text used as the body of a Note when this object is referenced # # By default this will be the class name and the result of calling diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 7a2bea567d..0888de62f0 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -12,7 +12,7 @@ # # # ... # -# participant :author, :assignee, :mentioned_users, :notes +# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) } # end # # issue = Issue.last @@ -39,12 +39,11 @@ module Participable # Save result into variable if you are going to reuse it inside same request def participants(current_user = self.author, project = self.project, load_lazy_references: true) participants = self.class.participant_attrs.flat_map do |attr| - meth = method(attr) value = - if attr == :mentioned_users - meth.call(current_user, load_lazy_references: false) + if attr.respond_to?(:call) + instance_exec(current_user, &attr) else - meth.call + send(attr) end participants_for(value, current_user, project) diff --git a/app/models/note.rb b/app/models/note.rb index de3b6df88f..2fbe478415 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Note < ActiveRecord::Base - include Mentionable include Gitlab::CurrentSettings include Participable + include Mentionable default_value_for :system, false attr_mentionable :note - participant :author, :mentioned_users + participant :author belongs_to :project belongs_to :noteable, polymorphic: true From 7b5ab3ded5e6f5d88f04802d5a71a9ae94d20f92 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 17:06:53 +0200 Subject: [PATCH 069/443] Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds --- CHANGELOG | 1 + app/models/ci/build.rb | 11 ++++++++- doc/ci/variables/README.md | 38 ++++++++++++++++++----------- spec/models/build_spec.rb | 34 +++++++++++++++++++++++--- spec/requests/ci/api/builds_spec.rb | 5 ++++ 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9e3d21f7e5..91ce558e2a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.1.0 (unreleased) - Add first and last to pagination (Zeger-Jan van de Weg) - Added Commit Status API - Show CI status on commit page + - Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds - Show CI status on Your projects page and Starred projects page - Remove "Continuous Integration" page from dashboard - Add notes and SSL verification entries to hook APIs (Ben Boeckel) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cfafbf6786..481440e869 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -119,7 +119,7 @@ module Ci end def variables - yaml_variables + project_variables + trigger_variables + predefined_variables + yaml_variables + project_variables + trigger_variables end def project @@ -258,5 +258,14 @@ module Ci [] end end + + def predefined_variables + variables = [] + variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag + variables << { key: :CI_BUILD_NAME, value: name, public: true } + variables << { key: :CI_BUILD_STAGE, value: stage, public: true } + variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request + variables + end end end diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 04c6bf1e3a..022afb7004 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -15,21 +15,27 @@ The API_TOKEN will take the Secure Variable value: `SECURE`. ### Predefined variables (Environment Variables) -| Variable | Description | +| Variable | Runner | Description | |-------------------------|-------------| -| **CI** | Mark that build is executed in CI environment | -| **GITLAB_CI** | Mark that build is executed in GitLab CI environment | -| **CI_SERVER** | Mark that build is executed in CI environment | -| **CI_SERVER_NAME** | CI server that is used to coordinate builds | -| **CI_SERVER_VERSION** | Not yet defined | -| **CI_SERVER_REVISION** | Not yet defined | -| **CI_BUILD_REF** | The commit revision for which project is built | -| **CI_BUILD_BEFORE_SHA** | The first commit that were included in push request | -| **CI_BUILD_REF_NAME** | The branch or tag name for which project is built | -| **CI_BUILD_ID** | The unique id of the current build that GitLab CI uses internally | -| **CI_BUILD_REPO** | The URL to clone the Git repository | -| **CI_PROJECT_ID** | The unique id of the current project that GitLab CI uses internally | -| **CI_PROJECT_DIR** | The full path where the repository is cloned and where the build is ran | +| **CI** | 0.4 | Mark that build is executed in CI environment | +| **GITLAB_CI** | all | Mark that build is executed in GitLab CI environment | +| **CI_SERVER** | all | Mark that build is executed in CI environment | +| **CI_SERVER_NAME** | all | CI server that is used to coordinate builds | +| **CI_SERVER_VERSION** | all | Not yet defined | +| **CI_SERVER_REVISION** | all | Not yet defined | +| **CI_BUILD_REF** | all | The commit revision for which project is built | +| **CI_BUILD_TAG** | 0.5 | The commit tag name. Present only when building tags. | +| **CI_BUILD_NAME** | 0.5 | The name of the build as defined in `.gitlab-ci.yml` | +| **CI_BUILD_STAGE** | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | +| **CI_BUILD_BEFORE_SHA** | all | The first commit that were included in push request | +| **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built | +| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally | +| **CI_BUILD_REPO** | all | The URL to clone the Git repository | +| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was triggered | +| **CI_PROJECT_ID** | all | The unique id of the current project that GitLab CI uses internally | +| **CI_PROJECT_DIR** | all | The full path where the repository is cloned and where the build is ran | + +**Some of the variables are only available when using runner with at least defined version.** Example values: @@ -39,6 +45,10 @@ export CI_BUILD_ID="50" export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a" export CI_BUILD_REF_NAME="master" export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git" +export CI_BUILD_TAG="1.0.0" +export CI_BUILD_NAME="spec:other" +export CI_BUILD_STAGE="test" +export CI_BUILD_TRIGGERED="true" export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" export CI_PROJECT_ID="34" export CI_SERVER="yes" diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d875015b99..7f999581ec 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -200,13 +200,34 @@ describe Ci::Build do context 'returns variables' do subject { build.variables } - let(:variables) do + let(:predefined_variables) do + [ + { key: :CI_BUILD_NAME, value: 'test', public: true }, + { key: :CI_BUILD_STAGE, value: 'stage', public: true }, + ] + end + + let(:yaml_variables) do [ { key: :DB_NAME, value: 'postgres', public: true } ] end - it { is_expected.to eq(variables) } + before { build.update_attributes(stage: 'stage') } + + it { is_expected.to eq(predefined_variables + yaml_variables) } + + context 'for tag' do + let(:tag_variable) do + [ + { key: :CI_BUILD_TAG, value: 'master', public: true } + ] + end + + before { build.update_attributes(tag: true) } + + it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) } + end context 'and secure variables' do let(:secure_variables) do @@ -219,7 +240,7 @@ describe Ci::Build do build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end - it { is_expected.to eq(variables + secure_variables) } + it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) } context 'and trigger variables' do let(:trigger) { FactoryGirl.create :ci_trigger, project: project } @@ -229,12 +250,17 @@ describe Ci::Build do { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } ] end + let(:predefined_trigger_variable) do + [ + { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } + ] + end before do build.trigger_request = trigger_request end - it { is_expected.to eq(variables + secure_variables + trigger_variables) } + it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) } end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 54c1d0199f..88218a93e1 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -76,6 +76,8 @@ describe Ci::API::API do expect(response.status).to eq(201) expect(json_response["variables"]).to eq([ + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, ]) @@ -93,6 +95,9 @@ describe Ci::API::API do expect(response.status).to eq(201) expect(json_response["variables"]).to eq([ + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, From 58074195198e715045dc5280aed9515297fe7ad3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 13 Oct 2015 17:00:55 +0200 Subject: [PATCH 070/443] Use tag? instead of tag to indicate that this is boolean --- app/models/ci/build.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 481440e869..23aed6d995 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -261,7 +261,7 @@ module Ci def predefined_variables variables = [] - variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag + variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag? variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request From a957eca6f364b7587175a6ffa647fc9df80abed9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 12:15:03 +0200 Subject: [PATCH 071/443] Added builds view --- CHANGELOG | 1 + .../javascripts/shortcuts_navigation.coffee | 1 + app/controllers/projects/builds_controller.rb | 25 ++++++++- app/helpers/gitlab_routing_helper.rb | 4 ++ app/helpers/projects_helper.rb | 4 ++ app/helpers/runners_helper.rb | 13 +++++ app/models/ability.rb | 2 + app/models/ci/runner.rb | 4 +- app/views/help/_shortcuts.html.haml | 6 +++ app/views/layouts/nav/_project.html.haml | 9 +++- .../layouts/nav/_project_settings.html.haml | 2 +- app/views/projects/builds/_build.html.haml | 52 +++++++++++++++++++ app/views/projects/builds/index.html.haml | 51 ++++++++++++++++++ config/routes.rb | 6 ++- spec/features/builds_spec.rb | 49 +++++++++++++++++ 15 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 app/views/projects/builds/_build.html.haml create mode 100644 app/views/projects/builds/index.html.haml diff --git a/CHANGELOG b/CHANGELOG index 9e3d21f7e5..8e71c8afb2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.1.0 (unreleased) - Fix cases where Markdown did not render links in activity feed (Stan Hu) - Add first and last to pagination (Zeger-Jan van de Weg) - Added Commit Status API + - Added Builds View - Show CI status on commit page - Show CI status on Your projects page and Starred projects page - Remove "Continuous Integration" page from dashboard diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index 5b6f9e7e3f..8decaedd87 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -7,6 +7,7 @@ class @ShortcutsNavigation extends Shortcuts Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity')) Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) + Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds')) Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 4e4ac6689d..0bcd9a8a36 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,11 +1,32 @@ class Projects::BuildsController < Projects::ApplicationController before_action :ci_project - before_action :build + before_action :build, except: [:index, :cancel_all] - before_action :authorize_admin_project!, except: [:show, :status] + before_action :authorize_admin_project!, except: [:index, :show, :status] layout "project" + def index + @scope = params[:scope] + @all_builds = project.ci_builds.order('created_at DESC').page(params[:page]).per(30) + + @builds = + case @scope + when 'pending' + @all_builds.pending + when 'running' + @all_builds.running + else + @all_builds + end + end + + def cancel_all + @project.ci_builds.running_or_pending.each(&:cancel) + + redirect_to namespace_project_builds_path(project.namespace, project) + end + def show @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC') @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 4d9da6ff83..b0b536d464 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -25,6 +25,10 @@ module GitlabRoutingHelper namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) end + def project_builds_path(project, *args) + namespace_project_builds_path(project.namespace, project, *args) + end + def activity_project_path(project, *args) activity_namespace_project_path(project.namespace, project, *args) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a0220af4c3..b7965aee87 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -113,6 +113,10 @@ module ProjectsHelper nav_tabs << :merge_requests end + if can?(current_user, :read_build, project) + nav_tabs << :builds + end + if can?(current_user, :admin_project, project) nav_tabs << :settings end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index 5d7d06c849..c13db93ae9 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -17,4 +17,17 @@ module RunnersHelper class: "fa fa-circle runner-status-#{status}", title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" end + + def runner_link(runner) + display_name = truncate(runner.display_name, length: 20) + id = "\##{runner.id}" + + if current_user && current_user.admin + link_to ci_admin_runner_path(runner) do + display_name + id + end + else + display_name + id + end + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 77c121ca5e..38bc208668 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -41,6 +41,7 @@ class Ability :read_project_member, :read_merge_request, :read_note, + :read_build, :download_code ] @@ -127,6 +128,7 @@ class Ability :read_project_member, :read_merge_request, :read_note, + :read_build, :create_project, :create_issue, :create_note diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6838ccfaaa..bc5cd137e9 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -56,7 +56,7 @@ module Ci end def display_name - return token unless !description.blank? + return short_sha unless !description.blank? description end @@ -78,7 +78,7 @@ module Ci end def short_sha - token[0...10] + token[0...8] end end end diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index e809d99ba7..67349fcbd7 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -99,6 +99,12 @@ .key c %td Go to commits + %tr + %td.shortcut + .key g + .key b + %td + Go to builds %tr %td.shortcut .key g diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index e4c285d802..f52d0ad9c0 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -32,12 +32,19 @@ Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches builds)) do + = nav_link(controller: %w(commit commits compare repositories tags branches)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span Commits + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do + = icon('link fw') + %span + Builds + - if project_nav_tab? :network = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 954dbe5d2b..b12dcd8411 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -66,7 +66,7 @@ %span CI Services = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project.gitlab_ci_project) do + = link_to ci_project_events_path(@project.ensure_gitlab_ci_project) do = icon('book fw') %span CI Events diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml new file mode 100644 index 0000000000..10b3ef7120 --- /dev/null +++ b/app/views/projects/builds/_build.html.haml @@ -0,0 +1,52 @@ +%tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.commit_status-link + - if build.target_url + = link_to build.target_url do + %strong Build ##{build.id} + - else + %strong Build ##{build.id} + + %td + = link_to namespace_project_commit_path(@project.namespace, @project, build.sha) do + = build.short_sha + + %td + = link_to namespace_project_commits_path(@project.namespace, @project, build.ref) do + = build.ref + + %td + - if build.runner + = runner_link(build.runner) + - else + .light none + + %td + = build.name + + .pull-right + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago + + %td + .pull-right + - if current_user && can?(current_user, :manage_builds, @project) + - if build.cancel_url + = link_to build.cancel_url, title: 'Cancel' do + %i.fa.fa-remove.cred diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml new file mode 100644 index 0000000000..2c6cf69c23 --- /dev/null +++ b/app/views/projects/builds/index.html.haml @@ -0,0 +1,51 @@ +- page_title "Builds" +- header_title project_title(@project, "Builds", project_builds_path(@project)) + +%ul.center-top-menu + %li{class: ('active' if @scope.nil?)} + = link_to project_builds_path(@project) do + All builds + %span.badge.js-totalbuilds-count= @all_builds.size + + %li{class: ('active' if @scope == 'pending')} + = link_to project_builds_path(@project, scope: :pending) do + Pending + %span.badge.js-pending-count= @all_builds.pending.size + + %li{class: ('active' if @scope == 'running')} + = link_to project_builds_path(@project, scope: :running) do + Running + %span.badge.js-running-count= @all_builds.running.size + +.gray-content-block + .oneline + List of all builds from this project + + - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + .pull-right + - if @all_builds.running_or_pending.any? + = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, method: :post, class: 'btn btn-danger' + +%ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Commit + %th Ref + %th Runner + %th Name + %th Duration + %th Finished at + %th + + - @builds.each do |build| + = render 'projects/builds/build', build: build + + = paginate @builds + diff --git a/config/routes.rb b/config/routes.rb index 8e6fbf6340..3253d950f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -585,7 +585,11 @@ Gitlab::Application.routes.draw do end end - resources :builds, only: [:show] do + resources :builds, only: [:index, :show] do + collection do + post :cancel_all + end + member do get :cancel get :status diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 924047a0d8..31f8aa8398 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -9,6 +9,55 @@ describe "Builds" do @gl_project.team << [@user, :master] end + describe "GET /:project/builds" do + context "All builds" do + before do + @build.success + visit namespace_project_builds_path(@gl_project.namespace, @gl_project) + end + + it { expect(page).to have_content 'All builds' } + it { expect(page).to have_content @build.short_sha } + it { expect(page).to have_content @build.ref } + it { expect(page).to have_content @build.name } + it { expect(page).to_not have_content 'Cancel all' } + end + + context "Pending scope" do + before do + @build.success + visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :pending) + end + + it { expect(page).to have_content 'No builds to show' } + it { expect(page).to_not have_content 'Cancel all' } + end + + context "Running scope" do + before do + @build.run! + visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :running) + end + + it { expect(page).to have_content 'Running' } + it { expect(page).to have_content 'Cancel all' } + it { expect(page).to have_content @build.short_sha } + it { expect(page).to have_content @build.ref } + it { expect(page).to have_content @build.name } + end + end + + describe "POST /:project/builds/:id/cancel_all" do + before do + @build.run! + visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content 'All builds' } + it { expect(page).to have_content 'canceled' } + it { expect(page).to_not have_content 'Cancel all' } + end + describe "GET /:project/builds/:id" do before do visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) From 09255eecd0812d35b09613a1cf2402d3108fcc49 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 14:07:41 +0200 Subject: [PATCH 072/443] Remove ordering from :ci_commits relation --- app/models/ci/commit.rb | 2 ++ app/models/ci/project.rb | 2 +- app/models/project.rb | 2 +- spec/models/ci/commit_spec.rb | 18 ++++++++++++++++++ spec/models/ci/project_spec.rb | 18 ------------------ 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 68864edfbb..cd45366b34 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -24,6 +24,8 @@ module Ci has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) } + validates_presence_of :sha validate :valid_commit_sha diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 88ba933a43..afd697f03f 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -205,7 +205,7 @@ module Ci end def commits - gl_project.ci_commits + gl_project.ci_commits.ordered end def builds diff --git a/app/models/project.rb b/app/models/project.rb index 021920008a..b99f340827 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -119,7 +119,7 @@ class Project < ActiveRecord::Base has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user - has_many :ci_commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 330971174f..d1cecce5a6 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -32,6 +32,24 @@ describe Ci::Commit do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + describe :ordered do + let(:project) { FactoryGirl.create :empty_project } + + it 'returns ordered list of commits' do + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project + expect(project.ci_commits.ordered).to eq([commit2, commit1]) + end + + it 'returns commits ordered by committed_at and id, with nulls last' do + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project + commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project + commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project + expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1]) + end + end + describe :last_build do subject { commit.last_build } before do diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index dec4720a71..81bb2a07cb 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -131,24 +131,6 @@ describe Ci::Project do end end - describe 'ordered commits' do - let(:project) { FactoryGirl.create :empty_project } - - it 'returns ordered list of commits' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project - expect(project.ci_commits).to eq([commit2, commit1]) - end - - it 'returns commits ordered by committed_at and id, with nulls last' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project - commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project - expect(project.ci_commits).to eq([commit2, commit4, commit3, commit1]) - end - end - context :valid_project do let(:commit) { FactoryGirl.create(:ci_commit) } From 4d69c6a3361bbc673e853995e3896d31241aa748 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 14:20:27 +0200 Subject: [PATCH 073/443] Refactor builds view --- app/controllers/projects/builds_controller.rb | 10 ++--- app/models/commit_status.rb | 1 + app/views/layouts/nav/_project.html.haml | 3 +- app/views/projects/builds/_build.html.haml | 6 +-- app/views/projects/builds/index.html.haml | 43 ++++++++++--------- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 0bcd9a8a36..b7d77c21e7 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController @builds = case @scope - when 'pending' - @all_builds.pending - when 'running' - @all_builds.running - else + when 'all' @all_builds + when 'finished' + @all_builds.finished + else + @all_builds.running_or_pending end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b4d91b1b0c..41eeb9a4ce 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -16,6 +16,7 @@ class CommitStatus < ActiveRecord::Base scope :success, -> { where(status: 'success') } scope :failed, -> { where(status: 'failed') } scope :running_or_pending, -> { where(status:[:running, :pending]) } + scope :finished, -> { where(status:[:success, :failed, :canceled]) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index f52d0ad9c0..53a913fe8f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -41,9 +41,10 @@ - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do - = icon('link fw') + = icon('cubes fw') %span Builds + %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all) - if project_nav_tab? :network = nav_link(controller: %w(network)) do diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml index 10b3ef7120..e74a3a6267 100644 --- a/app/views/projects/builds/_build.html.haml +++ b/app/views/projects/builds/_build.html.haml @@ -10,12 +10,10 @@ %strong Build ##{build.id} %td - = link_to namespace_project_commit_path(@project.namespace, @project, build.sha) do - = build.short_sha + = link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha) %td - = link_to namespace_project_commits_path(@project.namespace, @project, build.ref) do - = build.ref + = link_to build.ref, namespace_project_commits_path(@project.namespace, @project, build.ref) %td - if build.runner diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 2c6cf69c23..56bb7b1117 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,31 +1,32 @@ - page_title "Builds" - header_title project_title(@project, "Builds", project_builds_path(@project)) -%ul.center-top-menu - %li{class: ('active' if @scope.nil?)} - = link_to project_builds_path(@project) do - All builds - %span.badge.js-totalbuilds-count= @all_builds.size - - %li{class: ('active' if @scope == 'pending')} - = link_to project_builds_path(@project, scope: :pending) do - Pending - %span.badge.js-pending-count= @all_builds.pending.size - - %li{class: ('active' if @scope == 'running')} - = link_to project_builds_path(@project, scope: :running) do - Running - %span.badge.js-running-count= @all_builds.running.size - -.gray-content-block - .oneline - List of all builds from this project - +.project-issuable-filter + .controls - if @ci_project && current_user && can?(current_user, :manage_builds, @project) - .pull-right + .pull-left.hidden-xs - if @all_builds.running_or_pending.any? = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, method: :post, class: 'btn btn-danger' + %ul.center-top-menu + %li{class: ('active' if @scope.nil?)} + = link_to project_builds_path(@project) do + Running + %span.badge.js-running-count= @all_builds.running_or_pending.size + + %li{class: ('active' if @scope == 'finished')} + = link_to project_builds_path(@project, scope: :finished) do + Finished + %span.badge.js-running-count= @all_builds.finished.size + + %li{class: ('active' if @scope == 'all')} + = link_to project_builds_path(@project, scope: :all) do + All + %span.badge.js-totalbuilds-count= @all_builds.size + +.gray-content-block + List of #{@scope || 'running'} builds from this project + %ul.content-list - if @builds.blank? %li From 7af4f5215e28927830cbc74d383cdfeb9e4ef587 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 21:12:31 +0200 Subject: [PATCH 074/443] Show warning if build doesn't have runners with specified tags or runners didn't connect recently Slightly refactor runner status detection: moving it to Runner class Signed-off-by: Kamil Trzcinski --- CHANGELOG | 1 + .../ci/admin/runners_controller.rb | 4 +- .../projects/runners_controller.rb | 2 +- app/helpers/runners_helper.rb | 26 ++--- app/models/ci/build.rb | 12 +++ app/models/ci/project.rb | 6 +- app/models/ci/runner.rb | 17 +++ app/models/commit_status.rb | 4 + app/services/ci/register_build_service.rb | 2 +- app/views/projects/builds/show.html.haml | 21 ++++ .../commit_statuses/_commit_status.html.haml | 4 + spec/models/build_spec.rb | 101 ++++++++++++++++++ spec/models/ci/project_spec.rb | 13 +++ spec/models/ci/runner_spec.rb | 65 +++++++++++ 14 files changed, 256 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9e3d21f7e5..04af9cc0f4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.1.0 (unreleased) - Move CI triggers page to project settings area - Move CI project settings page to CE project settings area - Fix bug when removed file was not appearing in merge request diff + - Show warning when build cannot be served by any of the available CI runners - Note the original location of a moved project when notifying users of the move - Improve error message when merging fails - Add support of multibyte characters in LDAP UID (Roman Petrov) diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb index 9a68add908..110954a612 100644 --- a/app/controllers/ci/admin/runners_controller.rb +++ b/app/controllers/ci/admin/runners_controller.rb @@ -6,7 +6,7 @@ module Ci @runners = Ci::Runner.order('id DESC') @runners = @runners.search(params[:search]) if params[:search].present? @runners = @runners.page(params[:page]).per(30) - @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count + @active_runners_cnt = Ci::Runner.online.count end def show @@ -66,7 +66,7 @@ module Ci end def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active) + params.require(:runner).permit(:token, :description, :tag_list, :active) end end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 6cb6e3ef6d..deb07a2141 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -60,6 +60,6 @@ class Projects::RunnersController < Projects::ApplicationController end def runner_params - params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) + params.require(:runner).permit(:description, :tag_list, :active) end end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index 5d7d06c849..f79fef03ae 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -1,20 +1,16 @@ module RunnersHelper def runner_status_icon(runner) - unless runner.contacted_at - return content_tag :i, nil, - class: "fa fa-warning-sign", - title: "New runner. Has not connected yet" + status = runner.status + case status + when :not_connected + content_tag :i, nil, + class: "fa fa-warning-sign", + title: "New runner. Has not connected yet" + + when :online, :offline, :paused + content_tag :i, nil, + class: "fa fa-circle runner-status-#{status}", + title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" end - - status = - if runner.active? - runner.contacted_at > 3.hour.ago ? :online : :offline - else - :paused - end - - content_tag :i, nil, - class: "fa fa-circle runner-status-#{status}", - title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cfafbf6786..2c7cad46b0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -231,6 +231,18 @@ module Ci end end + def can_be_served?(runner) + (tag_list - runner.tag_list).empty? + end + + def any_runners_online? + project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } + end + + def show_warning? + pending? && !any_runners_online? + end + private def yaml_variables diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 88ba933a43..ef28353a30 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -115,12 +115,12 @@ module Ci web_url end - def any_runners? - if runners.active.any? + def any_runners?(&block) + if runners.active.any?(&block) return true end - shared_runners_enabled && Ci::Runner.shared.active.any? + shared_runners_enabled && Ci::Runner.shared.active.any?(&block) end def set_default_values diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6838ccfaaa..02a3e9db1f 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -20,6 +20,8 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model + + LAST_CONTACT_TIME = 5.minutes.ago has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -33,6 +35,7 @@ module Ci scope :shared, ->() { where(is_shared: true) } scope :active, ->() { where(active: true) } scope :paused, ->() { where(active: false) } + scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } acts_as_taggable @@ -65,6 +68,20 @@ module Ci is_shared end + def online? + contacted_at && contacted_at > LAST_CONTACT_TIME + end + + def status + if contacted_at.nil? + :not_connected + elsif active? + online? ? :online : :offline + else + :paused + end + end + def belongs_to_one_project? runner_projects.count == 1 end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b4d91b1b0c..92905c618e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -88,4 +88,8 @@ class CommitStatus < ActiveRecord::Base def retry_url nil end + + def show_warning? + false + end end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 71b61bbe38..7beb098659 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -17,7 +17,7 @@ module Ci builds = builds.order('created_at ASC') build = builds.find do |build| - (build.tag_list - current_runner.tag_list).empty? + build.can_be_served?(current_runner) end diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 9c3ae622b7..70a93eb597 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -39,6 +39,27 @@ .pull-right = @build.updated_at.stamp('19:00 Aug 27') + - if @build.show_warning? + - unless @build.any_runners_online? + .bs-callout.bs-callout-warning + %p + - if no_runners_for_project?(@build.project) + This build is stuck, because the project doesn't have runners assigned. + - elsif @build.tags.any? + This build is stuck. + %br + This build is stuck, because you don't have any active runners online with these tags assigned to the project: + - @build.tags.each do |tag| + %span.label.label-primary + = tag + - else + This build is stuck, because you don't have any active runners online that can run this build. + + %br + Go to + = link_to namespace_project_runners_path(@build.gl_project.namespace, @build.gl_project) do + Runners page + .row.prepend-top-default .col-md-9 .clearfix diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 7314f8e79d..99fdde4540 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -2,6 +2,10 @@ %td.status = ci_status_with_icon(commit_status.status) + - if commit_status.show_warning? + .pull-right + %i.fa.fa-warning-sign.text-warning + %td.commit_status-link - if commit_status.target_url = link_to commit_status.target_url do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d875015b99..6047171a6f 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -273,4 +273,105 @@ describe Ci::Build do is_expected.to eq(['rec1', pusher_email]) end end + + describe :can_be_served? do + let(:runner) { FactoryGirl.create :ci_specific_runner } + + before { build.project.runners << runner } + + context 'runner without tags' do + it 'can handle builds without tags' do + expect(build.can_be_served?(runner)).to be_truthy + end + + it 'cannot handle build with tags' do + build.tag_list = ['aa'] + expect(build.can_be_served?(runner)).to be_falsey + end + end + + context 'runner with tags' do + before { runner.tag_list = ['bb', 'cc'] } + + it 'can handle builds without tags' do + expect(build.can_be_served?(runner)).to be_truthy + end + + it 'can handle build with matching tags' do + build.tag_list = ['bb'] + expect(build.can_be_served?(runner)).to be_truthy + end + + it 'cannot handle build with not matching tags' do + build.tag_list = ['aa'] + expect(build.can_be_served?(runner)).to be_falsey + end + end + end + + describe :any_runners_online? do + subject { build.any_runners_online? } + + context 'when no runners' do + it { is_expected.to be_falsey } + end + + context 'if there are runner' do + let(:runner) { FactoryGirl.create :ci_specific_runner } + + before do + build.project.runners << runner + runner.update_attributes(contacted_at: 1.second.ago) + end + + it { is_expected.to be_truthy } + + it 'that is inactive' do + runner.update_attributes(active: false) + is_expected.to be_falsey + end + + it 'that is not online' do + runner.update_attributes(contacted_at: nil) + is_expected.to be_falsey + end + + it 'that cannot handle build' do + expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false) + is_expected.to be_falsey + end + + end + end + + describe :show_warning? do + subject { build.show_warning? } + + %w(pending).each do |state| + context "if commit_status.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_truthy } + + context "and there are specific runner" do + let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago } + + before do + build.project.runners << runner + runner.save + end + + it { is_expected.to be_falsey } + end + end + end + + %w(success failed canceled running).each do |state| + context "if commit_status.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_falsey } + end + end + end end diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index dec4720a71..c1605d6885 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -259,5 +259,18 @@ describe Ci::Project do FactoryGirl.create(:ci_shared_runner) expect(project.any_runners?).to be_falsey end + + it "checks the presence of specific runner" do + project = FactoryGirl.create(:ci_project) + specific_runner = FactoryGirl.create(:ci_specific_runner) + project.runners << specific_runner + expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy + end + + it "checks the presence of shared runner" do + project = FactoryGirl.create(:ci_project, shared_runners_enabled: true) + shared_runner = FactoryGirl.create(:ci_shared_runner) + expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy + end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 757593a7ab..536a737a33 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -48,6 +48,71 @@ describe Ci::Runner do it { expect(shared_runner.only_for?(project)).to be_truthy } end + describe :online do + subject { Ci::Runner.online } + + before do + @runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago) + @runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) + end + + it { is_expected.to eq([@runner2])} + end + + describe :online? do + let(:runner) { FactoryGirl.create(:ci_shared_runner) } + + subject { runner.online? } + + context 'never contacted' do + before { runner.contacted_at = nil } + + it { is_expected.to be_falsey } + end + + context 'contacted long time ago time' do + before { runner.contacted_at = 1.year.ago } + + it { is_expected.to be_falsey } + end + + context 'contacted 1s ago' do + before { runner.contacted_at = 1.second.ago } + + it { is_expected.to be_truthy } + end + end + + describe :status do + let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) } + + subject { runner.status } + + context 'never connected' do + before { runner.contacted_at = nil } + + it { is_expected.to eq(:not_connected) } + end + + context 'contacted 1s ago' do + before { runner.contacted_at = 1.second.ago } + + it { is_expected.to eq(:online) } + end + + context 'contacted long time ago' do + before { runner.contacted_at = 1.year.ago } + + it { is_expected.to eq(:offline) } + end + + context 'inactive' do + before { runner.active = false } + + it { is_expected.to eq(:paused) } + end + end + describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do runner = FactoryGirl.create(:ci_specific_runner) From 16bfd9d31a4e79e68591ba85428384512a93b680 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 13 Oct 2015 02:41:25 +0200 Subject: [PATCH 075/443] Fix specs --- spec/helpers/runners_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb index b3d635a193..35f91b7dec 100644 --- a/spec/helpers/runners_helper_spec.rb +++ b/spec/helpers/runners_helper_spec.rb @@ -12,7 +12,7 @@ describe RunnersHelper do end it "returns online text" do - runner = FactoryGirl.build(:ci_runner, contacted_at: 1.hour.ago, active: true) + runner = FactoryGirl.build(:ci_runner, contacted_at: 1.second.ago, active: true) expect(runner_status_icon(runner)).to include("Runner is online") end end From d9e2232f6393a1627847c9f6c2be3c4f987d0797 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 13:51:29 +0200 Subject: [PATCH 076/443] Fix warning sign --- app/helpers/runners_helper.rb | 2 +- app/views/projects/builds/show.html.haml | 2 +- .../projects/commit_statuses/_commit_status.html.haml | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index f79fef03ae..5afebab88e 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -4,7 +4,7 @@ module RunnersHelper case status when :not_connected content_tag :i, nil, - class: "fa fa-warning-sign", + class: "fa fa-warning", title: "New runner. Has not connected yet" when :online, :offline, :paused diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 70a93eb597..91c1b16c9f 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -25,7 +25,7 @@ %a Build ##{@build.id} · - %i.fa.fa-warning-sign + %i.fa.fa-warning This build was retried. .gray-content-block.second-block diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 99fdde4540..637154f56a 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -2,10 +2,6 @@ %td.status = ci_status_with_icon(commit_status.status) - - if commit_status.show_warning? - .pull-right - %i.fa.fa-warning-sign.text-warning - %td.commit_status-link - if commit_status.target_url = link_to commit_status.target_url do @@ -13,6 +9,9 @@ - else %strong Build ##{commit_status.id} + - if commit_status.show_warning? + %i.fa.fa-warning.text-warning + %td = commit_status.ref From d9ece71ef0677a1d3468697485db7cbcf1b83745 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 14:21:49 +0200 Subject: [PATCH 077/443] Fix specs --- app/controllers/projects/builds_controller.rb | 12 ++--- app/models/ci/runner.rb | 2 +- features/steps/project/commits/commits.rb | 2 +- spec/features/builds_spec.rb | 50 +++++++++---------- spec/models/ci/runner_spec.rb | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index b7d77c21e7..54c01ddf23 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController @builds = case @scope - when 'all' - @all_builds - when 'finished' - @all_builds.finished - else - @all_builds.running_or_pending + when 'all' + @all_builds + when 'finished' + @all_builds.finished + else + @all_builds.running_or_pending end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index bc5cd137e9..52fdc2578e 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -78,7 +78,7 @@ module Ci end def short_sha - token[0...8] + token[0...8] if token end end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index a3cb83880e..e5b3f27135 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -113,7 +113,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I click status link' do - click_link "Builds" + find('.commit-ci-menu').click_link "Builds" end step 'I see builds list' do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 31f8aa8398..a339a15111 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -10,33 +10,10 @@ describe "Builds" do end describe "GET /:project/builds" do - context "All builds" do - before do - @build.success - visit namespace_project_builds_path(@gl_project.namespace, @gl_project) - end - - it { expect(page).to have_content 'All builds' } - it { expect(page).to have_content @build.short_sha } - it { expect(page).to have_content @build.ref } - it { expect(page).to have_content @build.name } - it { expect(page).to_not have_content 'Cancel all' } - end - - context "Pending scope" do - before do - @build.success - visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :pending) - end - - it { expect(page).to have_content 'No builds to show' } - it { expect(page).to_not have_content 'Cancel all' } - end - context "Running scope" do before do @build.run! - visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :running) + visit namespace_project_builds_path(@gl_project.namespace, @gl_project) end it { expect(page).to have_content 'Running' } @@ -45,6 +22,29 @@ describe "Builds" do it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } end + + context "Finished scope" do + before do + @build.run! + visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :finished) + end + + it { expect(page).to have_content 'No builds to show' } + it { expect(page).to have_content 'Cancel all' } + end + + context "All builds" do + before do + @gl_project.ci_builds.running_or_pending.each(&:success) + visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :all) + end + + it { expect(page).to have_content 'All' } + it { expect(page).to have_content @build.short_sha } + it { expect(page).to have_content @build.ref } + it { expect(page).to have_content @build.name } + it { expect(page).to_not have_content 'Cancel all' } + end end describe "POST /:project/builds/:id/cancel_all" do @@ -53,7 +53,7 @@ describe "Builds" do visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) end - it { expect(page).to have_content 'All builds' } + it { expect(page).to have_content 'All' } it { expect(page).to have_content 'canceled' } it { expect(page).to_not have_content 'Cancel all' } end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 757593a7ab..a401ae7fe5 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -32,7 +32,7 @@ describe Ci::Runner do end it 'should return the token if the description is an empty string' do - runner = FactoryGirl.build(:ci_runner, description: '') + runner = FactoryGirl.build(:ci_runner, description: '', token: 'token') expect(runner.display_name).to eq runner.token end end From 8f584d5f2c8dc036214a7fc71ce864fe23b4cb9e Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 6 Oct 2015 17:09:03 +0300 Subject: [PATCH 078/443] Fix: Images cannot show when projects' path was changed --- CHANGELOG | 1 + app/models/namespace.rb | 2 + app/models/project.rb | 2 + app/services/projects/transfer_service.rb | 4 + app/uploaders/file_uploader.rb | 2 +- features/steps/admin/projects.rb | 2 + lib/gitlab/markdown.rb | 2 + lib/gitlab/markdown/upload_link_filter.rb | 47 ++++++++++++ lib/gitlab/uploads_transfer.rb | 35 +++++++++ public/uploads/.gitkeep | 0 .../projects/uploads_controller_spec.rb | 4 +- .../gitlab/email/attachment_uploader_spec.rb | 1 - .../markdown/upload_link_filter_spec.rb | 75 +++++++++++++++++++ spec/lib/gitlab/uploads_transfer_spec.rb | 50 +++++++++++++ .../projects/download_service_spec.rb | 2 - .../projects/transfer_service_spec.rb | 2 + spec/services/projects/upload_service_spec.rb | 4 - 17 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 lib/gitlab/markdown/upload_link_filter.rb create mode 100644 lib/gitlab/uploads_transfer.rb delete mode 100644 public/uploads/.gitkeep create mode 100644 spec/lib/gitlab/markdown/upload_link_filter_spec.rb create mode 100644 spec/lib/gitlab/uploads_transfer_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 31feb115d1..87baa6b662 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ v 8.1.0 (unreleased) - Add "New Page" button to Wiki Pages tab (Stan Hu) - Only render 404 page from /public - Hide passwords from services API (Alex Lossent) + - Fix: Images cannot show when projects' path was changed v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bc8525df5a..5782e649f8 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -118,6 +118,8 @@ class Namespace < ActiveRecord::Base gitlab_shell.add_namespace(path_was) if gitlab_shell.mv_namespace(path_was, path) + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir diff --git a/app/models/project.rb b/app/models/project.rb index 021920008a..cd30467fae 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -656,6 +656,8 @@ class Project < ActiveRecord::Base # db changes in order to prevent out of sync between db and fs raise Exception.new('repository cannot be renamed') end + + Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end def hook_attrs diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index c327c244f0..64ea6dd42e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -27,6 +27,7 @@ module Projects def transfer(project, new_namespace) Project.transaction do old_path = project.path_with_namespace + old_namespace = project.namespace new_path = File.join(new_namespace.try(:path) || '', project.path) if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? @@ -51,6 +52,9 @@ module Projects # clear project cached events project.reset_events_cache + # Move uploads + Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + true end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index f9673abbfe..e821158583 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -26,7 +26,7 @@ class FileUploader < CarrierWave::Uploader::Base end def secure_url - File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename) + File.join("/uploads", @secret, file.filename) end def file_storage? diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 17233f89f3..5a1cc9aa15 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -41,6 +41,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end step 'I transfer project to group \'Web\'' do + allow_any_instance_of(Projects::TransferService). + to receive(:move_uploads_to_new_namespace).and_return(true) find(:xpath, "//input[@id='new_namespace_id']").set group.id click_button 'Transfer' end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index ae5f254469..32a368c2e2 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -48,6 +48,7 @@ module Gitlab autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' + autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' # Public: Parse the provided text with GitLab-Flavored Markdown # @@ -140,6 +141,7 @@ module Gitlab Gitlab::Markdown::SyntaxHighlightFilter, Gitlab::Markdown::SanitizationFilter, + Gitlab::Markdown::UploadLinkFilter, Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/upload_link_filter.rb new file mode 100644 index 0000000000..fbada73ab8 --- /dev/null +++ b/lib/gitlab/markdown/upload_link_filter.rb @@ -0,0 +1,47 @@ +require 'gitlab/markdown' +require 'html/pipeline/filter' +require 'uri' + +module Gitlab + module Markdown + # HTML filter that "fixes" relative upload links to files. + # Context options: + # :project (required) - Current project + # + class UploadLinkFilter < HTML::Pipeline::Filter + def call + doc.search('a').each do |el| + process_link_attr el.attribute('href') + end + + doc.search('img').each do |el| + process_link_attr el.attribute('src') + end + + doc + end + + protected + + def process_link_attr(html_attr) + return if html_attr.blank? + + uri = html_attr.value + if uri.starts_with?("/uploads/") + html_attr.value = build_url(uri).to_s + end + end + + def build_url(uri) + File.join(Gitlab.config.gitlab.url, context[:project].path_with_namespace, uri) + end + + # Ensure that a :project key exists in context + # + # Note that while the key might exist, its value could be nil! + def validate + needs :project + end + end + end +end diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb new file mode 100644 index 0000000000..be8fcc7b2d --- /dev/null +++ b/lib/gitlab/uploads_transfer.rb @@ -0,0 +1,35 @@ +module Gitlab + class UploadsTransfer + def move_project(project_path, namespace_path_was, namespace_path) + new_namespace_folder = File.join(root_dir, namespace_path) + FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) + from = File.join(root_dir, namespace_path_was, project_path) + to = File.join(root_dir, namespace_path, project_path) + move(from, to, "") + end + + def rename_project(path_was, path, namespace_path) + base_dir = File.join(root_dir, namespace_path) + move(path_was, path, base_dir) + end + + def rename_namespace(path_was, path) + move(path_was, path) + end + + private + + def move(path_was, path, base_dir = nil) + base_dir = root_dir unless base_dir + from = File.join(base_dir, path_was) + to = File.join(base_dir, path) + FileUtils.mv(from, to) + rescue Errno::ENOENT + false + end + + def root_dir + File.join(Rails.root, "public", "uploads") + end + end +end diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f51abfedae..93c4494c66 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -33,7 +33,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"/uploads" expect(response.body).to match '\"is_image\":true' end end @@ -49,7 +49,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"/uploads" expect(response.body).to match '\"is_image\":false' end end diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index e8208e15e2..8fb432367b 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -13,7 +13,6 @@ describe Gitlab::Email::AttachmentUploader do expect(link).not_to be_nil expect(link[:is_image]).to be_truthy expect(link[:alt]).to eq("bricks") - expect(link[:url]).to include("/#{project.path_with_namespace}") expect(link[:url]).to include("bricks.png") end end diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb new file mode 100644 index 0000000000..9ae45a6f55 --- /dev/null +++ b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb @@ -0,0 +1,75 @@ +# encoding: UTF-8 + +require 'spec_helper' + +module Gitlab::Markdown + describe UploadLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def image(path) + %() + end + + def link(path) + %(#{path}) + end + + let(:project) { create(:project) } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a link' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'rebuilds relative URL for an image' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = '/uploads/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" + end + end + end +end diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb new file mode 100644 index 0000000000..260364a513 --- /dev/null +++ b/spec/lib/gitlab/uploads_transfer_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::UploadsTransfer do + before do + @root_dir = File.join(Rails.root, "public", "uploads") + @upload_transfer = Gitlab::UploadsTransfer.new + + @project_path_was = "test_project_was" + @project_path = "test_project" + @namespace_path_was = "test_namespace_was" + @namespace_path = "test_namespace" + end + + after do + FileUtils.rm_rf([ + File.join(@root_dir, @namespace_path), + File.join(@root_dir, @namespace_path_was) + ]) + end + + describe '#move_project' do + it "moves project upload to another namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_project' do + it "renames project" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was)) + @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_namespace' do + it "renames namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end +end diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index f12e09c58c..ddee2e62df 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -37,7 +37,6 @@ describe Projects::DownloadService do it { expect(@link_to_file).to have_key('url') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to be true } - it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('rails_sample.jpg') } it { expect(@link_to_file['alt']).to eq('rails_sample') } end @@ -52,7 +51,6 @@ describe Projects::DownloadService do it { expect(@link_to_file).to have_key('url') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to be false } - it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('doc_sample.txt') } it { expect(@link_to_file['alt']).to eq('doc_sample.txt') } end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index bb7da33b12..47755bfc99 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -7,6 +7,8 @@ describe Projects::TransferService do context 'namespace -> namespace' do before do + allow_any_instance_of(Gitlab::UploadsTransfer). + to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index fa4ff6b01a..1b1a80d1fe 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -18,7 +18,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('banana_sample') } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('banana_sample.gif') } end @@ -34,7 +33,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_value('dk') } it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('dk.png') } end @@ -49,7 +47,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('rails_sample') } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } end @@ -64,7 +61,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('doc_sample.txt') } it { expect(@link_to_file[:is_image]).to equal(false) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } end From b83a18a55cd03cfa6d0d631b8d4567ae29b5fe26 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 14 Oct 2015 19:21:27 +0300 Subject: [PATCH 079/443] Revert "Improve invalidation of stored service password if the endpoint URL is changed" This reverts commit b46397548056e4e8ef00efe4f641c61ba1dd5230. --- app/controllers/admin/services_controller.rb | 8 +--- .../projects/services_controller.rb | 8 +--- app/models/project_services/bamboo_service.rb | 2 +- .../project_services/teamcity_service.rb | 2 +- app/models/service.rb | 32 +++------------ spec/models/service_spec.rb | 39 ++----------------- 6 files changed, 13 insertions(+), 78 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 4613358833..a62170662e 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -39,13 +39,7 @@ class Admin::ServicesController < Admin::ApplicationController end def application_services_params - application_services_params = params.permit(:id, + params.permit(:id, service: Projects::ServicesController::ALLOWED_PARAMS) - if application_services_params[:service].is_a?(Hash) - Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param| - application_services_params[:service].delete(param) if application_services_params[:service][param].blank? - end - end - application_services_params end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 129068ef01..3047ee8a1f 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] - - # Parameters to ignore if no value is specified - FILTER_BLANK_PARAMS = [:password] - # Authorize before_action :authorize_admin_project! before_action :service, only: [:edit, :update, :test] @@ -63,9 +59,7 @@ class Projects::ServicesController < Projects::ApplicationController def service_params service_params = params.require(:service).permit(ALLOWED_PARAMS) - FILTER_BLANK_PARAMS.each do |param| - service_params.delete(param) if service_params[param].blank? - end + service_params.delete("password") if service_params["password"].blank? service_params end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 4a18c772e5..5f5255ab48 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -48,7 +48,7 @@ class BambooService < CiService end def reset_password - if prop_modified?(:bamboo_url) && !prop_updated?(:password) + if prop_updated?(:bamboo_url) self.password = nil end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a548a557df..fb11cad352 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -45,7 +45,7 @@ class TeamcityService < CiService end def reset_password - if prop_modified?(:teamcity_url) && !prop_updated?(:password) + if prop_updated?(:teamcity_url) self.password = nil end end diff --git a/app/models/service.rb b/app/models/service.rb index 946ea1a096..7e845d565b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -33,8 +33,6 @@ class Service < ActiveRecord::Base after_initialize :initialize_properties - after_commit :reset_updated_properties - belongs_to :project has_one :service_hook @@ -105,7 +103,6 @@ class Service < ActiveRecord::Base # Provide convenient accessor methods # for each serialized property. - # Also keep track of updated properties. def self.prop_accessor(*args) args.each do |arg| class_eval %{ @@ -114,36 +111,19 @@ class Service < ActiveRecord::Base end def #{arg}=(value) - updated_properties['#{arg}'] = #{arg} unless updated_properties.include?('#{arg}') self.properties['#{arg}'] = value end } end end - # Returns a hash of the properties that have been assigned a new value since last save, - # indicating their original values (attr => original value). - # ActiveRecord does not provide a mechanism to track changes in serialized keys, - # so we need a specific implementation for service properties. - # This allows to track changes to properties set with the accessor methods, - # but not direct manipulation of properties hash. - def updated_properties - @updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new - end - + # ActiveRecord does not provide a mechanism to track changes in serialized keys. + # This is why we need to perform extra query to do it mannually. def prop_updated?(prop_name) - # Check if a new value was set. - # The new value may or may not be the same as the old value - updated_properties.include?(prop_name) - end - - def prop_modified?(prop_name) - # Check if new value was set and it is different from the old value - prop_updated?(prop_name) && updated_properties[prop_name] != send(prop_name) - end - - def reset_updated_properties - @updated_properties = nil + relation_name = self.type.underscore + previous_value = project.send(relation_name).send(prop_name) + return false if previous_value.nil? + previous_value != send(prop_name) end def async_execute(data) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index d42b96294b..da87ea5b84 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -116,47 +116,14 @@ describe Service do ) end - it "returns false when the property has not been assigned a new value" do + it "returns false" do service.username = "key_changed" expect(service.prop_updated?(:bamboo_url)).to be_falsy end - it "returns true when the property has been assigned a different value" do - service.bamboo_url = "http://example.com" + it "returns true" do + service.bamboo_url = "http://other.com" expect(service.prop_updated?(:bamboo_url)).to be_truthy end - - it "returns true when the property has been re-assigned the same value" do - service.bamboo_url = 'http://gitlab.com' - expect(service.prop_updated?(:bamboo_url)).to be_truthy - end - end - - describe "#prop_modified?" do - let(:service) do - BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "returns false when the property has not been assigned a new value" do - service.username = "key_changed" - expect(service.prop_modified?(:bamboo_url)).to be_falsy - end - - it "returns true when the property has been assigned a different value" do - service.bamboo_url = "http://example.com" - expect(service.prop_modified?(:bamboo_url)).to be_truthy - end - - it "returns false when the property has been re-assigned the same value" do - service.bamboo_url = 'http://gitlab.com' - expect(service.prop_modified?(:bamboo_url)).to be_falsy - end end end From 0ff8975939846a3d013421d1f46baf3cbb77dec2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 19:57:20 +0200 Subject: [PATCH 080/443] Fix cancel_all specs --- app/views/projects/builds/index.html.haml | 2 +- config/routes.rb | 2 +- spec/features/builds_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 56bb7b1117..b04784025f 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -6,7 +6,7 @@ - if @ci_project && current_user && can?(current_user, :manage_builds, @project) .pull-left.hidden-xs - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, method: :post, class: 'btn btn-danger' + = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' %ul.center-top-menu %li{class: ('active' if @scope.nil?)} diff --git a/config/routes.rb b/config/routes.rb index 3253d950f2..06abc2820a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -587,7 +587,7 @@ Gitlab::Application.routes.draw do resources :builds, only: [:index, :show] do collection do - post :cancel_all + get :cancel_all end member do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index a339a15111..941e45408a 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -47,10 +47,10 @@ describe "Builds" do end end - describe "POST /:project/builds/:id/cancel_all" do + describe "GET /:project/builds/:id/cancel_all" do before do @build.run! - visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit cancel_all_namespace_project_builds_path(@gl_project.namespace, @gl_project) end it { expect(page).to have_content 'All' } From ddeb766e9638d14dd86d966ef097fbeebb9dffab Mon Sep 17 00:00:00 2001 From: Mike Kelly Date: Wed, 14 Oct 2015 18:42:59 +0000 Subject: [PATCH 081/443] Remove some padding from code blocks This creates a weird "leading indent" on the first line of code blocks, at least in Chrome 46, and ends up making the first row not line up nicely with everything else. --- app/assets/stylesheets/framework/typography.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index bf36f96cc9..b56758a97d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -17,7 +17,6 @@ font-family: $monospace_font; white-space: pre; word-wrap: normal; - padding: 1px 2px; } kbd { @@ -268,4 +267,4 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; -} +} \ No newline at end of file From fc0d92746d8a228077c6602be1f77b679f5d196a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 14:55:40 -0400 Subject: [PATCH 082/443] Prevent a JS error in MergeRequestTabs When `window.location.hash` is pointing to a note, e.g. `#note_1234`, `scrollToElement` would throw an error because a selector such as `.commits #note_1234` doesn't exist, so `offset()` returned `undefined`. This error would prevent subsequent calls from running, which caused the loading spinner to never be hidden. Now we ensure the selector returns a valid element before trying to scroll to it. --- app/assets/javascripts/merge_request_tabs.js.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 3e77ea515f..593a8f4213 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -68,8 +68,8 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - top = $(container + " " + window.location.hash).offset().top - $('body').scrollTo(top) + $el = $("#{container} #{window.location.hash}") + $('body').scrollTo($el.offset().top) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -127,7 +127,7 @@ class @MergeRequestTabs document.getElementById('commits').innerHTML = data.html $('.js-timeago').timeago() @commitsLoaded = true - @scrollToElement(".commits") + @scrollToElement("#commits") loadDiff: (source) -> return if @diffsLoaded @@ -137,7 +137,7 @@ class @MergeRequestTabs success: (data) => document.getElementById('diffs').innerHTML = data.html @diffsLoaded = true - @scrollToElement(".diffs") + @scrollToElement("#diffs") # Show or hide the loading spinner # From 386b13d624e065920fd5730d9d6a0d7983f005cb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 15:38:19 -0400 Subject: [PATCH 083/443] Allow highlight themes to override `pre` colors --- app/assets/stylesheets/framework/typography.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index bf36f96cc9..6ccc084526 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -101,9 +101,9 @@ pre { margin: 12px 0 12px 0 !important; - background-color: #f8fafc !important; + background-color: #f8fafc; font-size: 13px !important; - color: #5b6169 !important; + color: #5b6169; line-height: 1.6em !important; @include border-radius(2px); } From d7f2a656f9b56a7d6531fd4eefcd747142b3034f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 15:39:02 -0400 Subject: [PATCH 084/443] Update highlight themes so they always have the correct colors --- app/assets/stylesheets/highlight/dark.scss | 11 +++++------ app/assets/stylesheets/highlight/monokai.scss | 13 ++++++------- .../stylesheets/highlight/solarized_dark.scss | 9 ++++----- .../stylesheets/highlight/solarized_light.scss | 9 ++++----- app/assets/stylesheets/highlight/white.scss | 18 +++++++----------- 5 files changed, 26 insertions(+), 34 deletions(-) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 8323a8598e..6a2b25ddc6 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,11 +1,10 @@ /* https://github.com/MozMorris/tomorrow-pygments */ -pre.code.highlight.dark, .code.dark { - background-color: #1d1f21; - color: #c5c8c6; + background-color: #1d1f21 !important; + color: #c5c8c6 !important; - pre.code, + pre.highlight, .line-numbers, .line-numbers a { background-color: #1d1f21 !important; @@ -23,8 +22,8 @@ pre.code.highlight.dark, // Search result highlight span.highlight_word { - background: #ffe792; - color: #000000; + background-color: #ffe792 !important; + color: #000000 !important; } .hll { background-color: #373b41 } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index e838167433..8560c3c490 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,15 +1,14 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ -pre.code.monokai, .code.monokai { - background: #272822; - color: #f8f8f2; + background-color: #272822 !important; + color: #f8f8f2 !important; pre.highlight, .line-numbers, .line-numbers a { - background:#272822 !important; - color:#f8f8f2 !important; + background-color :#272822 !important; + color: #f8f8f2 !important; } pre.code { @@ -23,8 +22,8 @@ pre.code.monokai, // Search result highlight span.highlight_word { - background: #ffe792; - color: #000000; + background-color: #ffe792 !important; + color: #000000 !important; } .hll { background-color: #49483e } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index bd41480aef..7d489a9666 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,11 +1,10 @@ /* https://gist.github.com/qguv/7936275 */ -pre.code.highlight.solarized-dark, .code.solarized-dark { - background-color: #002b36; - color: #93a1a1; + background-color: #002b36 !important; + color: #93a1a1 !important; - pre.code, + pre.highlight, .line-numbers, .line-numbers a { background-color: #002b36 !important; @@ -23,7 +22,7 @@ pre.code.highlight.solarized-dark, // Search result highlight span.highlight_word { - background: #094554; + background-color: #094554 !important; } /* Solarized Dark diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 4cc6286387..200ed34644 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -1,11 +1,10 @@ /* https://gist.github.com/qguv/7936275 */ -pre.code.highlight.solarized-light, .code.solarized-light { - background-color: #fdf6e3; - color: #586e75; + background-color: #fdf6e3 !important; + color: #586e75 !important; - pre.code, + pre.highlight, .line-numbers, .line-numbers a { background-color: #fdf6e3 !important; @@ -23,7 +22,7 @@ pre.code.highlight.solarized-light, // Search result highlight span.highlight_word { - background: #eee8d5; + background-color: #eee8d5 !important; } /* Solarized Light diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 20a144ef95..e2626da787 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,24 +1,20 @@ /* https://github.com/aahan/pygments-github-style */ -pre.code.highlight.white, .code.white { - background-color: #f8fafc; - font-size: 13px; - color: #5b6169; - line-height: 1.6em; + background-color: #f8fafc !important; + color: #5b6169 !important; + + pre.highlight, .line-numbers, .line-numbers a { background-color: $background-color !important; color: $gl-gray !important; } - pre.highlight { - background-color: #fff !important; - color: #333 !important; - } - pre.code { border-left: 1px solid $border-color; + background-color: #fff !important; + color: #333 !important; } // highlight line via anchor @@ -28,7 +24,7 @@ pre.code.highlight.white, // Search result highlight span.highlight_word { - background: #fafe3d; + background-color: #fafe3d !important; } .hll { background-color: #f8f8f8 } From 9750de4943f044499982d3d0b57425525ce5edc9 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 16:30:19 -0400 Subject: [PATCH 085/443] Simplify the `issues_sentence` helper --- app/helpers/merge_requests_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 81773e7afc..728d877ace 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -47,7 +47,7 @@ module MergeRequestsHelper end def issues_sentence(issues) - issues.map { |i| "##{i.iid}" }.to_sentence + issues.map(&:to_reference).to_sentence end def mr_change_branches_path(merge_request) From 459b882131f5da02e86e89abde6c84dfee2d587c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 14 Oct 2015 16:31:54 -0400 Subject: [PATCH 086/443] Shut up, Rubocop [ci skip] --- 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 432bf026ba..d5493ca038 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -269,4 +269,4 @@ if Rails.env.test? Settings.gitlab['default_projects_limit'] = 42 Settings.gitlab['default_can_create_group'] = true Settings.gitlab['default_can_create_team'] = false -end \ No newline at end of file +end From 4271a2b8acf85f2ca31b0e377db5154a38576487 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 20:55:17 +0200 Subject: [PATCH 087/443] Fix tests --- spec/features/builds_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 941e45408a..154857e77f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -53,8 +53,7 @@ describe "Builds" do visit cancel_all_namespace_project_builds_path(@gl_project.namespace, @gl_project) end - it { expect(page).to have_content 'All' } - it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'No builds to show' } it { expect(page).to_not have_content 'Cancel all' } end From f7ed469ece5e113273b006dd64cc0d29243cfcde Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Thu, 15 Oct 2015 15:24:26 +1100 Subject: [PATCH 088/443] change capitalisation in buttons --- app/views/projects/snippets/_actions.html.haml | 6 ++++-- app/views/projects/snippets/show.html.haml | 2 +- app/views/snippets/_actions.html.haml | 7 +++++-- app/views/snippets/show.html.haml | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 4d5f702637..4a51546942 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,8 +1,10 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do = icon('plus') - new snippet + New Snippet - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do + = icon('trash-o') + Delete - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do = icon('pencil-square-o') diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 4f236fcc7e..b6a8e68af2 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -9,6 +9,6 @@ = @snippet.file_name .file-actions .btn-group - = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index a4fdf89a4e..751fafa894 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,7 +1,10 @@ = link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do = icon('plus') - new snippet -= link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' + New Snippet +- if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do + = icon('trash-o') + Delete - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do = icon('pencil-square-o') diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 202a43bc57..612d7b65d8 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -8,5 +8,5 @@ = @snippet.file_name .file-actions .btn-group - = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' From 45e11d95f27584f699392fad8f54c1807e562d7f Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Thu, 15 Oct 2015 17:01:17 +1100 Subject: [PATCH 089/443] fix spinach features to use new button wordings Also fixed an accidental deletion of pratial --- app/views/projects/snippets/show.html.haml | 1 + features/project/snippets.feature | 2 +- features/snippets/snippets.feature | 2 +- features/steps/project/snippets.rb | 6 +++--- features/steps/snippets/snippets.rb | 6 +++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index b6a8e68af2..1aeb0da201 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -11,4 +11,5 @@ .btn-group = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/features/project/snippets.feature b/features/project/snippets.feature index 77e42a1a38..270557cbde 100644 --- a/features/project/snippets.feature +++ b/features/project/snippets.feature @@ -30,5 +30,5 @@ Feature: Project Snippets Scenario: I destroy "Snippet one" Given I visit snippet page "Snippet one" - And I click link "Remove Snippet" + And I click link "Delete" Then I should not see "Snippet one" in snippets diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 4f617b6bed..e15d7c7934 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -24,7 +24,7 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" - And I click link "Destroy" + And I click link "Delete" Then I should not see "Personal snippet one" in snippets Scenario: I create new internal snippet diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index db8ad08bb9..811ded6955 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -42,13 +42,13 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "Edit"' do - page.within ".file-title" do + page.within ".page-title" do click_link "Edit" end end - step 'I click link "Remove Snippet"' do - click_link "remove" + step 'I click link "Delete"' do + click_link "Delete" end step 'I submit new snippet "Snippet three"' do diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 6ff48e0c6b..80d1ddeef0 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -13,13 +13,13 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I click link "Edit"' do - page.within ".file-title" do + page.within ".page-title" do click_link "Edit" end end - step 'I click link "Destroy"' do - click_link "remove" + step 'I click link "Delete"' do + click_link "Delete" end step 'I submit new snippet "Personal snippet three"' do From 83f04853e9a749c3397ee7683a78b986e1070904 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 15 Oct 2015 11:14:39 +0200 Subject: [PATCH 090/443] Update gitlab_git to 7.2.19 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 4938cbf8b8..d81cc4d540 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.17' +gem "gitlab_git", '~> 7.2.19' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 1dd56cd9c8..f9421331e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.1) gemojione (~> 2.0) - gitlab_git (7.2.17) + gitlab_git (7.2.19) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -836,7 +836,7 @@ DEPENDENCIES gitlab-flowdock-git-hook (~> 1.0.1) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.17) + gitlab_git (~> 7.2.19) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) From 9fa209e1d8748b1eb1ef041bb1c40d30914f2815 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 10:47:05 +0200 Subject: [PATCH 091/443] Remove unneeded change --- app/views/layouts/nav/_project_settings.html.haml | 2 +- app/views/projects/builds/_build.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index b12dcd8411..954dbe5d2b 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -66,7 +66,7 @@ %span CI Services = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project.ensure_gitlab_ci_project) do + = link_to ci_project_events_path(@project.gitlab_ci_project) do = icon('book fw') %span CI Events diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml index e74a3a6267..ff146f7389 100644 --- a/app/views/projects/builds/_build.html.haml +++ b/app/views/projects/builds/_build.html.haml @@ -29,9 +29,9 @@ - build.tags.each do |tag| %span.label.label-primary = tag - - if build.try(:trigger_request) + - if build.trigger_request %span.label.label-info triggered - - if build.try(:allow_failure) + - if build.allow_failure %span.label.label-danger allowed to fail %td.duration From 72f428c7d217a5c40ed87d68ab9100e4c8754633 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 8 Oct 2015 13:28:26 +0200 Subject: [PATCH 092/443] Improve performance of User.by_login Performance is improved in two steps: 1. On PostgreSQL an expression index is used for checking lower(email) and lower(username). 2. The check to determine if we're searching for a username or Email is moved to Ruby. Thanks to @haynes for suggesting and writing the initial implementation of this. Moving the check to Ruby makes this method an additional 1.5 times faster compared to doing the check in the SQL query. With performance being improved I've now also tweaked the amount of iterations required by the User.by_login benchmark. This method now runs between 900 and 1000 iterations per second. --- CHANGELOG | 1 + app/models/user.rb | 10 ++++++++-- ...32_add_users_lower_username_email_indexes.rb | 17 +++++++++++++++++ lib/tasks/migrate/setup_postgresql.rake | 2 ++ spec/benchmarks/models/user_spec.rb | 4 +++- 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20151008110232_add_users_lower_username_email_indexes.rb diff --git a/CHANGELOG b/CHANGELOG index 07ff3d6de4..12561d6cee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) - Make diff file view easier to use on mobile screens (Stan Hu) + - Improved performance of finding users by username or Email address - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) diff --git a/app/models/user.rb b/app/models/user.rb index 889d2d3b86..17ccb3b878 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -68,6 +68,7 @@ class User < ActiveRecord::Base include Referable include Sortable include TokenAuthenticatable + include CaseSensitivity default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group @@ -273,8 +274,13 @@ class User < ActiveRecord::Base end def by_login(login) - where('lower(username) = :value OR lower(email) = :value', - value: login.to_s.downcase).first + return nil unless login + + if login.include?('@'.freeze) + unscoped.iwhere(email: login).take + else + unscoped.iwhere(username: login).take + end end def find_by_username!(username) diff --git a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb new file mode 100644 index 0000000000..2f2dc77678 --- /dev/null +++ b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb @@ -0,0 +1,17 @@ +class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));' + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :users, :index_on_users_lower_username + remove_index :users, :index_on_users_lower_email + end +end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index bf6894a835..141a0b74ec 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -1,6 +1,8 @@ require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') +require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') desc 'GitLab | Sets up PostgreSQL' task setup_postgresql: :environment do NamespacesProjectsPathLowerIndexes.new.up + AddUsersLowerUsernameEmailIndexes.new.up end diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb index 168be20b7a..cc5c390419 100644 --- a/spec/benchmarks/models/user_spec.rb +++ b/spec/benchmarks/models/user_spec.rb @@ -11,7 +11,9 @@ describe User, benchmark: true do end end - let(:iterations) { 1000 } + # The iteration count is based on the query taking little over 1 ms when + # using PostgreSQL. + let(:iterations) { 900 } describe 'using a capitalized username' do benchmark_subject { User.by_login('Alice') } From 693e63f5234032aa1abbc226c0d6337d6ea810ed Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 12 Oct 2015 16:58:09 +0200 Subject: [PATCH 093/443] Allow avatar_icon to operate on a User If the User object is already known before calling this method being able to re-use said object can save us an extra SQL query. --- app/helpers/application_helper.rb | 10 +++++++--- spec/helpers/application_helper_spec.rb | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cab2278adb..8ecdeaf8e7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,13 +68,17 @@ module ApplicationHelper end end - def avatar_icon(user_email = '', size = nil) - user = User.find_by(email: user_email) + def avatar_icon(user_or_email = nil, size = nil) + if user_or_email.is_a?(User) + user = user_or_email + else + user = User.find_by(email: user_or_email) + end if user user.avatar_url(size) || default_avatar else - gravatar_icon(user_email, size) + gravatar_icon(user_or_email, size) end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 742420f550..1dfae0fbd3 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -99,6 +99,15 @@ describe ApplicationHelper do helper.avatar_icon('foo@example.com', 20) end + + describe 'using a User' do + it 'should return an URL for the avatar' do + user = create(:user, avatar: File.open(avatar_file_path)) + + expect(helper.avatar_icon(user).to_s). + to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") + end + end end describe 'gravatar_icon' do From 949635636728fa388d983b180b7ab4e55aa8caa9 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 12 Oct 2015 17:03:12 +0200 Subject: [PATCH 094/443] Re-use User objects for avatar_icon where possible This removes the need for running an extra SQL query in these cases. --- app/views/admin/users/show.html.haml | 2 +- app/views/dashboard/milestones/_issue.html.haml | 2 +- app/views/dashboard/milestones/_merge_request.html.haml | 2 +- app/views/dashboard/milestones/show.html.haml | 2 +- app/views/groups/group_members/_group_member.html.haml | 2 +- app/views/groups/milestones/_issue.html.haml | 2 +- app/views/groups/milestones/_merge_request.html.haml | 2 +- app/views/groups/milestones/show.html.haml | 2 +- app/views/layouts/_page.html.haml | 2 +- app/views/layouts/ci/_page.html.haml | 2 +- app/views/profiles/show.html.haml | 2 +- app/views/projects/milestones/_issue.html.haml | 2 +- app/views/projects/milestones/_merge_request.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- app/views/projects/project_members/_project_member.html.haml | 2 +- app/views/users/show.html.haml | 4 ++-- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a383ea5738..231bcb0426 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -8,7 +8,7 @@ = @user.name %ul.well-list %li - = image_tag avatar_icon(@user.email, 60), class: "avatar s60" + = image_tag avatar_icon(@user, 60), class: "avatar s60" %li %span.light Profile page: %strong diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml index f689b9698e..1408ebdd5d 100644 --- a/app/views/dashboard/milestones/_issue.html.haml +++ b/app/views/dashboard/milestones/_issue.html.haml @@ -7,4 +7,4 @@ = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml index 8f5c4cce52..77c46de030 100644 --- a/app/views/dashboard/milestones/_merge_request.html.haml +++ b/app/views/dashboard/milestones/_merge_request.html.haml @@ -7,4 +7,4 @@ = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 0d204ced7e..d5c4a44fef 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -79,7 +79,7 @@ - @dashboard_milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user.email, 32), class: "avatar s32" + = image_tag avatar_icon(user, 32), class: "avatar s32" %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.username diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index b5f359279d..3c19381321 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -5,7 +5,7 @@ %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} - if member.user - = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml index 09f9b4b896..9b85d83d6d 100644 --- a/app/views/groups/milestones/_issue.html.haml +++ b/app/views/groups/milestones/_issue.html.haml @@ -7,4 +7,4 @@ = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml index d0d1426762..e3aa4aad19 100644 --- a/app/views/groups/milestones/_merge_request.html.haml +++ b/app/views/groups/milestones/_merge_request.html.haml @@ -7,4 +7,4 @@ = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 0c213f4218..c6cde97c58 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -87,7 +87,7 @@ - @group_milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user.email, 32), class: "avatar s32" + = image_tag avatar_icon(user, 32), class: "avatar s32" %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.username diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1a883e20e8..352b8040cf 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -18,7 +18,7 @@ = render partial: 'layouts/collapse_button' - if current_user = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36' + = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml index bb5ec727bf..ab3e29c3f4 100644 --- a/app/views/layouts/ci/_page.html.haml +++ b/app/views/layouts/ci/_page.html.haml @@ -15,7 +15,7 @@ = render partial: 'layouts/collapse_button' - if current_user = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36' + = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 47412e2ef0..ac7355dde1 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -68,7 +68,7 @@ .col-md-5 .light-well - = image_tag avatar_icon(@user.email, 160), alt: '', class: 'avatar s160' + = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' .clearfix .profile-avatar-form-option diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index 88fccfe498..133d802aac 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,7 +1,7 @@ %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' %span = link_to [@project.namespace.becomes(Namespace), @project, issue] do %span.cgray ##{issue.iid} diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index 0d7a118569..a1033607c5 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -5,4 +5,4 @@ = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 4eeb0621e5..3a898dfbcf 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -104,7 +104,7 @@ - @users.each do |user| %li = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user.email, 32), class: "avatar s32" + = image_tag avatar_icon(user, 32), class: "avatar s32" %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.username diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 860a997cff..76c46d1d80 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -4,7 +4,7 @@ %li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} %span.list-item-name - if member.user - = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 11beb3e323..2a64708d07 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -9,8 +9,8 @@ .row %section.col-md-7 .header-with-avatar - = link_to avatar_icon(@user.email, 400), target: '_blank' do - = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 90), class: "avatar avatar-tile s90", alt: '' %h3 = @user.name - if @user == current_user From ff8f7fb0a111a5db2a50aa40e967cae699b0b245 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 12 Oct 2015 17:22:22 +0200 Subject: [PATCH 095/443] Re-use User for avatars in link_to_member --- app/helpers/projects_helper.rb | 2 +- spec/helpers/projects_helper_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a0220af4c3..6dc2c381c2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -29,7 +29,7 @@ module ProjectsHelper author_html = "" # Build avatar image tag - author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] + author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] # Build name span tag author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 53e56ebff4..f2efb528ae 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -70,4 +70,18 @@ describe ProjectsHelper do expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-nil-readme") end end + + describe 'link_to_member' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, group: group) } + let(:user) { create(:user) } + + describe 'using the default options' do + it 'returns an HTML link to the user' do + link = helper.link_to_member(project, user) + + expect(link).to match(%r{/u/#{user.username}}) + end + end + end end From 1554786c6ac49b452697d2f7a3e8daf6e3ac36d3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 13 Oct 2015 11:49:01 +0200 Subject: [PATCH 096/443] Eager load various issue/note associations This ensures we don't end up running N+1 queries for the objects in the affected collections. --- app/controllers/projects/issues_controller.rb | 2 +- app/models/concerns/issuable.rb | 7 ++++++- app/models/group.rb | 2 +- app/models/note.rb | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4612abcbae..27aa70a992 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -57,7 +57,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.inc_author.fresh + @notes = @issue.notes.inc_associations.fresh @noteable = @issue respond_with(@issue) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 4db4ffb2e7..0e8bcc1a4e 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -47,7 +47,8 @@ module Issuable prefix: true attr_mentionable :title, :description - participant :author, :assignee, :notes, :mentioned_users + + participant :author, :assignee, :notes_with_associations, :mentioned_users end module ClassMethods @@ -176,6 +177,10 @@ module Issuable self.class.to_s.underscore end + def notes_with_associations + notes.includes(:author, :project) + end + private def filter_superceded_votes(votes, notes) diff --git a/app/models/group.rb b/app/models/group.rb index 9cd146bb73..465c22d23a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -64,7 +64,7 @@ class Group < Namespace end def owners - @owners ||= group_members.owners.map(&:user) + @owners ||= group_members.owners.includes(:user).map(&:user) end def add_users(user_ids, access_level, current_user = nil) diff --git a/app/models/note.rb b/app/models/note.rb index ee0c14598f..3ad9895a93 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -59,6 +59,7 @@ class Note < ActiveRecord::Base scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :inc_associations, ->{ includes(:author, :noteable, :updated_by) } serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } From 39fcd445fa5a6af19ead78b47de84a199e7e7d50 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 13 Oct 2015 11:49:50 +0200 Subject: [PATCH 097/443] Use note.author for issue comment avatars This removes the need for running a query to find the User object again based on the supplied Email address. --- app/views/projects/notes/_note.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 1638ad6891..71347faea9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: '' + = image_tag avatar_icon(note.author), class: 'avatar s40', alt: '' .timeline-content .note-header - if note_editable?(note) From fa3d7db39077611703edf7d328e33f0049f5d341 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 13 Oct 2015 11:54:06 +0200 Subject: [PATCH 098/443] Added Bullet to the Gemfile This can be used to resolve N+1 query problems. Bullet is disabled by default and can be enabled by starting Rails with the environment variable ENABLE_BULLET set to a non empty value (e.g. "true"). --- Gemfile | 1 + Gemfile.lock | 5 +++++ config/initializers/bullet.rb | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 config/initializers/bullet.rb diff --git a/Gemfile b/Gemfile index 9b2416ab45..d3c1fd50e3 100644 --- a/Gemfile +++ b/Gemfile @@ -224,6 +224,7 @@ group :development do gem 'quiet_assets', '~> 1.0.2' gem 'rack-mini-profiler', '~> 0.9.0', require: false gem 'rerun', '~> 0.10.0' + gem 'bullet', require: false # Better errors handler gem 'better_errors', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 8cc400aa55..48804dba8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,6 +87,9 @@ GEM terminal-table (~> 1.4) browser (1.0.0) builder (3.2.2) + bullet (4.14.9) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.9.0) byebug (6.0.2) cal-heatmap-rails (0.0.1) capybara (2.4.4) @@ -755,6 +758,7 @@ GEM unicorn-worker-killer (0.4.3) get_process_mem (~> 0) unicorn (~> 4) + uniform_notifier (1.9.0) uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) @@ -800,6 +804,7 @@ DEPENDENCIES bootstrap-sass (~> 3.0) brakeman (= 3.0.1) browser (~> 1.0.0) + bullet byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.4.0) diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb new file mode 100644 index 0000000000..95e82966c7 --- /dev/null +++ b/config/initializers/bullet.rb @@ -0,0 +1,6 @@ +if ENV['ENABLE_BULLET'] + require 'bullet' + + Bullet.enable = true + Bullet.console = true +end From 7971ed5daca4bfb1310d6458f323377391a99429 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 13 Oct 2015 17:21:15 +0200 Subject: [PATCH 099/443] Added active_record_query_trace This can be used to track down where queries originate from, regardless of whether they're caused by N+1 problems or not. This can be enabled by setting the environment variable ENABLE_QUERY_TRACE to a non-empty value (e.g. "true"). --- Gemfile | 1 + Gemfile.lock | 2 ++ config/initializers/active_record_query_trace.rb | 5 +++++ 3 files changed, 8 insertions(+) create mode 100644 config/initializers/active_record_query_trace.rb diff --git a/Gemfile b/Gemfile index d3c1fd50e3..0524803f31 100644 --- a/Gemfile +++ b/Gemfile @@ -225,6 +225,7 @@ group :development do gem 'rack-mini-profiler', '~> 0.9.0', require: false gem 'rerun', '~> 0.10.0' gem 'bullet', require: false + gem 'active_record_query_trace', require: false # Better errors handler gem 'better_errors', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 48804dba8e..ed7ea5bb78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,6 +17,7 @@ GEM activesupport (= 4.1.12) builder (~> 3.1) erubis (~> 2.7.0) + active_record_query_trace (1.5) activemodel (4.1.12) activesupport (= 4.1.12) builder (~> 3.1) @@ -788,6 +789,7 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) ace-rails-ap (~> 2.0.1) + active_record_query_trace activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) diff --git a/config/initializers/active_record_query_trace.rb b/config/initializers/active_record_query_trace.rb new file mode 100644 index 0000000000..4b3c2803b3 --- /dev/null +++ b/config/initializers/active_record_query_trace.rb @@ -0,0 +1,5 @@ +if ENV['ENABLE_QUERY_TRACE'] + require 'active_record_query_trace' + + ActiveRecordQueryTrace.enabled = 'true' +end From d4832b0341643d90df8323a5564521d3bcd3abc1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 12:21:57 +0200 Subject: [PATCH 100/443] Added rack-lineprof for development This can be used to measure the time (roughly) spent on a per line basis. This can also be used to measure timings for views, for example by adding the following to a URL: ?lineprof=app/views/projects/notes/_note rack-lineprof is only enabled when: 1. The application runs in development mode 2. The used Ruby is MRI 3. The environment variable ENABLE_LINEPROF is set to a non-empty value --- Gemfile | 1 + Gemfile.lock | 8 +++++++ config/environments/development.rb | 2 +- config/initializers/rack_lineprof.rb | 31 ++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 config/initializers/rack_lineprof.rb diff --git a/Gemfile b/Gemfile index 0524803f31..a58fc34bb7 100644 --- a/Gemfile +++ b/Gemfile @@ -226,6 +226,7 @@ group :development do gem 'rerun', '~> 0.10.0' gem 'bullet', require: false gem 'active_record_query_trace', require: false + gem 'rack-lineprof', platform: :mri # Better errors handler gem 'better_errors', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index ed7ea5bb78..d7feaa0ec5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -138,6 +138,7 @@ GEM daemons (1.2.3) database_cleaner (1.4.1) debug_inspector (0.0.2) + debugger-ruby_core_source (1.3.8) default_value_for (3.0.1) activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.4) @@ -506,6 +507,10 @@ GEM rack-attack (4.3.0) rack rack-cors (0.4.0) + rack-lineprof (0.0.3) + rack (~> 1.5) + rblineprof (~> 0.3.6) + term-ansicolor (~> 1.3) rack-mini-profiler (0.9.7) rack (>= 1.1.3) rack-mount (0.8.3) @@ -544,6 +549,8 @@ GEM rb-fsevent (0.9.5) rb-inotify (0.9.5) ffi (>= 0.5.0) + rblineprof (0.3.6) + debugger-ruby_core_source (~> 1.3) rbvmomi (1.8.2) builder nokogiri (>= 1.4.1) @@ -889,6 +896,7 @@ DEPENDENCIES quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) rack-cors (~> 0.4.0) + rack-lineprof rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) rails (= 4.1.12) diff --git a/config/environments/development.rb b/config/environments/development.rb index d7d6aed160..827a110c24 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,7 +24,7 @@ Gitlab::Application.configure do # Expands the lines which load the assets # config.assets.debug = true - + # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb new file mode 100644 index 0000000000..80d232c3d3 --- /dev/null +++ b/config/initializers/rack_lineprof.rb @@ -0,0 +1,31 @@ +# The default colors of rack-lineprof can be very hard to look at in terminals +# with darker backgrounds. This patch tweaks the colors a bit so the output is +# actually readable. +if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF'] + Gitlab::Application.config.middleware.use(Rack::Lineprof) + + module Rack + class Lineprof + class Sample < Rack::Lineprof::Sample.superclass + def format(*) + formatted = if level == CONTEXT + sprintf " | % 3i %s", line, code + else + sprintf "% 8.1fms %5i | % 3i %s", ms, calls, line, code + end + + case level + when CRITICAL + color.red formatted + when WARNING + color.yellow formatted + when NOMINAL + color.white formatted + else # CONTEXT + formatted + end + end + end + end + end +end From 8237da0d4a250b4cb07e85caac3c43e11e282ebb Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 12:44:10 +0200 Subject: [PATCH 101/443] Eager load note projects when viewing issues --- app/models/note.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/note.rb b/app/models/note.rb index 3ad9895a93..d0b30c5579 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -59,7 +59,10 @@ class Note < ActiveRecord::Base scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } - scope :inc_associations, ->{ includes(:author, :noteable, :updated_by) } + + scope :inc_associations, -> do + includes(:author, :noteable, :updated_by, :project) + end serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } From b5f8161daeefeaa66e810e9dddec43959333d8a7 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 14:53:06 +0200 Subject: [PATCH 102/443] Eager load project associations for notes This ensures that when viewing an issue each note already has the associated project, project members, group and group members available. Since this information is requres for every note this results in quite the reduction of SQL queries being executed. --- app/models/note.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/note.rb b/app/models/note.rb index d0b30c5579..196512c471 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -61,7 +61,8 @@ class Note < ActiveRecord::Base scope :inc_author, ->{ includes(:author) } scope :inc_associations, -> do - includes(:author, :noteable, :updated_by, :project) + includes(:author, :noteable, :updated_by, + project: [:project_members, {group: [:group_members]}]) end serialize :st_diff From 3025b711419fd1813baea5e2a2925c6ccf8d455a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 14:54:04 +0200 Subject: [PATCH 103/443] Improve ProjectTeam#max_member_access performance By comparing objects in Ruby we can greatly improve the performance of this method. In the worst case (should no data be eager loaded) this will run the same amount of queries as before, in the best case (when data _is_ eager loadeD) it requires no queries at all. The added benchmark used to produce around 273 iterations per second. With this commit this has been increased to almost 40 000 iterations per second: a speedup of roughly 145 times. Combined with eager loading Note associations this results in about 30 queries less when viewing a single issue, this in turn cuts down the loading time by 30-40%. --- app/models/project_team.rb | 21 +++++++++++++++---- spec/benchmarks/models/project_team_spec.rb | 23 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 spec/benchmarks/models/project_team_spec.rb diff --git a/app/models/project_team.rb b/app/models/project_team.rb index f602a96536..9f380a382c 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -139,15 +139,28 @@ class ProjectTeam Gitlab::Access.options.key max_member_access(user_id) end + # This method assumes project and group members are eager loaded for optimal + # performance. def max_member_access(user_id) access = [] - access << project.project_members.find_by(user_id: user_id).try(:access_field) - if group - access << group.group_members.find_by(user_id: user_id).try(:access_field) + project.project_members.each do |member| + if member.user_id == user_id + access << member.access_field if member.access_field + break + end end - access.compact.max + if group + group.group_members.each do |member| + if member.user_id == user_id + access << member.access_field if member.access_field + break + end + end + end + + access.max end private diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb new file mode 100644 index 0000000000..8b039ef731 --- /dev/null +++ b/spec/benchmarks/models/project_team_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe ProjectTeam, benchmark: true do + describe '#max_member_access' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, group: group) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + + 5.times do + project.team << [create(:user), :reporter] + + project.group.add_user(create(:user), :reporter) + end + end + + benchmark_subject { project.team.max_member_access(user.id) } + + it { is_expected.to iterate_per_second(35000) } + end +end From f3980e230f31d6589592b965700936a7bf2a9731 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 16:03:22 +0200 Subject: [PATCH 104/443] Don't use link_to/image_tag where not needed In these particular instances we can just use HAML tags directly. This can shave off some time spent loading issue pages, though this depends on the amount of comments being displayed. When viewing https://gitlab.com/gitlab-org/gitlab-ce/issues/2164 locally these changes reduce loading time by about 400 ms in total. --- app/views/projects/_md_preview.html.haml | 4 ++-- app/views/projects/_zen.html.haml | 6 +++--- app/views/projects/notes/_note.html.haml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 507757f6a2..7b21095ea3 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -2,10 +2,10 @@ .md-header.clearfix %ul.center-top-menu %li.active - = link_to '#md-write-holder', class: 'js-md-write-button', tabindex: '-1' do + %a.js-md-write-button(href="#md-write-holder" tabindex="-1") Write %li - = link_to '#md-preview-holder', class: 'js-md-preview-button', tabindex: '-1' do + %a.js-md-preview-button(href="md-preview-holder" tabindex="-1") Preview - if defined?(referenced_users) && referenced_users diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 6a41cdbc90..63ebfc9381 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,10 +1,10 @@ .zennable - %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } + %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") .zen-backdrop - classes << ' js-gfm-input markdown-area' = f.text_area attr, class: classes, placeholder: '' - = link_to nil, class: 'zen-enter-link', tabindex: '-1' do + %a.zen-enter-link(tabindex="-1" href="#") %i.fa.fa-expand Edit in fullscreen - = link_to nil, class: 'zen-leave-link' do + %a.zen-leave-link(href="#") %i.fa.fa-compress diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 71347faea9..5d18473079 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,8 +1,8 @@ %li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } } .timeline-entry-inner .timeline-icon - = link_to user_path(note.author) do - = image_tag avatar_icon(note.author), class: 'avatar s40', alt: '' + %a{href: user_path(note.author)} + %img.avatar.s40{src: avatar_icon(note.author), alt: ''} .timeline-content .note-header - if note_editable?(note) @@ -25,7 +25,7 @@ = '@' + note.author.username %span.note-last-update - = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do + %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') - if note.updated_at != note.created_at %span From 9f5811116ce56afe8bd2e46d92523e7240670016 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 14 Oct 2015 17:16:16 +0200 Subject: [PATCH 105/443] Changelog entry for issue page speedups --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 07ff3d6de4..52437a241b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) + - Speed up load times of issue detail pages by roughly 1.5x - Make diff file view easier to use on mobile screens (Stan Hu) - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) From e5925d073ea09072790856da1569865d5c45e408 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 15 Oct 2015 10:41:09 +0200 Subject: [PATCH 106/443] Renamed Note.inc_associations to with_associations --- app/controllers/projects/issues_controller.rb | 2 +- app/models/note.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 27aa70a992..97485c101f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -57,7 +57,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.inc_associations.fresh + @notes = @issue.notes.with_associations.fresh @noteable = @issue respond_with(@issue) diff --git a/app/models/note.rb b/app/models/note.rb index 196512c471..dc110f4dc3 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -60,7 +60,7 @@ class Note < ActiveRecord::Base scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } - scope :inc_associations, -> do + scope :with_associations, -> do includes(:author, :noteable, :updated_by, project: [:project_members, {group: [:group_members]}]) end From bed29940efc0c76dd966b26acb5dfb00d61e601e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 15 Oct 2015 10:42:48 +0200 Subject: [PATCH 107/443] Fixed Rubocop styling issues --- app/models/note.rb | 2 +- config/initializers/rack_lineprof.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index dc110f4dc3..ebbdd2f33a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -62,7 +62,7 @@ class Note < ActiveRecord::Base scope :with_associations, -> do includes(:author, :noteable, :updated_by, - project: [:project_members, {group: [:group_members]}]) + project: [:project_members, { group: [:group_members] }]) end serialize :st_diff diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb index 80d232c3d3..f0c006d811 100644 --- a/config/initializers/rack_lineprof.rb +++ b/config/initializers/rack_lineprof.rb @@ -9,10 +9,10 @@ if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF'] class Sample < Rack::Lineprof::Sample.superclass def format(*) formatted = if level == CONTEXT - sprintf " | % 3i %s", line, code - else - sprintf "% 8.1fms %5i | % 3i %s", ms, calls, line, code - end + sprintf " | % 3i %s", line, code + else + sprintf "% 8.1fms %5i | % 3i %s", ms, calls, line, code + end case level when CRITICAL From 98e666ab6a61ef67c2ba15d31839fd1cf414d587 Mon Sep 17 00:00:00 2001 From: Alex Lossent Date: Thu, 15 Oct 2015 09:09:01 +0200 Subject: [PATCH 108/443] Improve invalidation of stored service password if the endpoint URL is changed Password can now be specified at the same time as the new URL, and the service template admin pages now work. --- app/controllers/admin/services_controller.rb | 8 +- .../projects/services_controller.rb | 8 +- app/models/project_services/bamboo_service.rb | 2 +- .../project_services/teamcity_service.rb | 2 +- app/models/service.rb | 35 ++++-- .../project_services/bamboo_service_spec.rb | 74 +++++++++--- .../project_services/teamcity_service_spec.rb | 75 +++++++++--- spec/models/service_spec.rb | 110 +++++++++++++++++- 8 files changed, 260 insertions(+), 54 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index a62170662e..4613358833 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -39,7 +39,13 @@ class Admin::ServicesController < Admin::ApplicationController end def application_services_params - params.permit(:id, + application_services_params = params.permit(:id, service: Projects::ServicesController::ALLOWED_PARAMS) + if application_services_params[:service].is_a?(Hash) + Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param| + application_services_params[:service].delete(param) if application_services_params[:service][param].blank? + end + end + application_services_params end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 3047ee8a1f..129068ef01 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,6 +9,10 @@ class Projects::ServicesController < Projects::ApplicationController :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] + + # Parameters to ignore if no value is specified + FILTER_BLANK_PARAMS = [:password] + # Authorize before_action :authorize_admin_project! before_action :service, only: [:edit, :update, :test] @@ -59,7 +63,9 @@ class Projects::ServicesController < Projects::ApplicationController def service_params service_params = params.require(:service).permit(ALLOWED_PARAMS) - service_params.delete("password") if service_params["password"].blank? + FILTER_BLANK_PARAMS.each do |param| + service_params.delete(param) if service_params[param].blank? + end service_params end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 5f5255ab48..d31b12f539 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -48,7 +48,7 @@ class BambooService < CiService end def reset_password - if prop_updated?(:bamboo_url) + if bamboo_url_changed? && !password_touched? self.password = nil end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index fb11cad352..0b02246125 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -45,7 +45,7 @@ class TeamcityService < CiService end def reset_password - if prop_updated?(:teamcity_url) + if teamcity_url_changed? && !password_touched? self.password = nil end end diff --git a/app/models/service.rb b/app/models/service.rb index 7e845d565b..d610abd168 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -33,6 +33,8 @@ class Service < ActiveRecord::Base after_initialize :initialize_properties + after_commit :reset_updated_properties + belongs_to :project has_one :service_hook @@ -103,6 +105,7 @@ class Service < ActiveRecord::Base # Provide convenient accessor methods # for each serialized property. + # Also keep track of updated properties in a similar way as ActiveModel::Dirty def self.prop_accessor(*args) args.each do |arg| class_eval %{ @@ -111,21 +114,39 @@ class Service < ActiveRecord::Base end def #{arg}=(value) + updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? self.properties['#{arg}'] = value end + + def #{arg}_changed? + #{arg}_touched? && #{arg} != #{arg}_was + end + + def #{arg}_touched? + updated_properties.include?('#{arg}') + end + + def #{arg}_was + updated_properties['#{arg}'] + end } end end - # ActiveRecord does not provide a mechanism to track changes in serialized keys. - # This is why we need to perform extra query to do it mannually. - def prop_updated?(prop_name) - relation_name = self.type.underscore - previous_value = project.send(relation_name).send(prop_name) - return false if previous_value.nil? - previous_value != send(prop_name) + # Returns a hash of the properties that have been assigned a new value since last save, + # indicating their original values (attr => original value). + # ActiveRecord does not provide a mechanism to track changes in serialized keys, + # so we need a specific implementation for service properties. + # This allows to track changes to properties set with the accessor methods, + # but not direct manipulation of properties hash. + def updated_properties + @updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new end + def reset_updated_properties + @updated_properties = nil + end + def async_execute(data) return unless supported_events.include?(data[:object_kind]) diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index f8a3493f52..c34b2487ec 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -30,27 +30,65 @@ describe BambooService, models: true do let(:user) { create(:user) } let(:project) { create(:project) } - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end + context "when a password was previously set" do + before do + @bamboo_service = BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "reset password if url changed" do + @bamboo_service.bamboo_url = 'http://gitlab1.com' + @bamboo_service.save + expect(@bamboo_service.password).to be_nil + end + + it "does not reset password if username changed" do + @bamboo_service.username = "some_name" + @bamboo_service.save + expect(@bamboo_service.password).to eq("password") + end - it "reset password if url changed" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil + it "does not reset password if new url is set together with password, even if it's the same password" do + @bamboo_service.bamboo_url = 'http://gitlab_edited.com' + @bamboo_service.password = 'password' + @bamboo_service.save + expect(@bamboo_service.password).to eq("password") + expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + end + + it "should reset password if url changed, even if setter called multiple times" do + @bamboo_service.bamboo_url = 'http://gitlab1.com' + @bamboo_service.bamboo_url = 'http://gitlab1.com' + @bamboo_service.save + expect(@bamboo_service.password).to be_nil + end end + + context "when no password was previously set" do + before do + @bamboo_service = BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic' + } + ) + end + + it "saves password if new url is set together with password" do + @bamboo_service.bamboo_url = 'http://gitlab_edited.com' + @bamboo_service.password = 'password' + @bamboo_service.save + expect(@bamboo_service.password).to eq("password") + expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + end - it "does not reset password if username changed" do - @bamboo_service.username = "some_name" - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") end end end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 3dbd2346bc..f26b47a856 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -30,27 +30,64 @@ describe TeamcityService, models: true do let(:user) { create(:user) } let(:project) { create(:project) } - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end + context "when a password was previously set" do + before do + @teamcity_service = TeamcityService.create( + project: create(:project), + properties: { + teamcity_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "reset password if url changed" do + @teamcity_service.teamcity_url = 'http://gitlab1.com' + @teamcity_service.save + expect(@teamcity_service.password).to be_nil + end + + it "does not reset password if username changed" do + @teamcity_service.username = "some_name" + @teamcity_service.save + expect(@teamcity_service.password).to eq("password") + end - it "reset password if url changed" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil - end + it "does not reset password if new url is set together with password, even if it's the same password" do + @teamcity_service.teamcity_url = 'http://gitlab_edited.com' + @teamcity_service.password = 'password' + @teamcity_service.save + expect(@teamcity_service.password).to eq("password") + expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + end - it "does not reset password if username changed" do - @teamcity_service.username = "some_name" - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") + it "should reset password if url changed, even if setter called multiple times" do + @teamcity_service.teamcity_url = 'http://gitlab1.com' + @teamcity_service.teamcity_url = 'http://gitlab1.com' + @teamcity_service.save + expect(@teamcity_service.password).to be_nil + end + end + + context "when no password was previously set" do + before do + @teamcity_service = TeamcityService.create( + project: create(:project), + properties: { + teamcity_url: 'http://gitlab.com', + username: 'mic' + } + ) + end + + it "saves password if new url is set together with password" do + @teamcity_service.teamcity_url = 'http://gitlab_edited.com' + @teamcity_service.password = 'password' + @teamcity_service.save + expect(@teamcity_service.password).to eq("password") + expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + end end end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index da87ea5b84..692e5fda3b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -104,7 +104,7 @@ describe Service do end end - describe "#prop_updated?" do + describe "{property}_changed?" do let(:service) do BambooService.create( project: create(:project), @@ -116,14 +116,112 @@ describe Service do ) end - it "returns false" do + it "returns false when the property has not been assigned a new value" do service.username = "key_changed" - expect(service.prop_updated?(:bamboo_url)).to be_falsy + expect(service.bamboo_url_changed?).to be_falsy end - it "returns true" do - service.bamboo_url = "http://other.com" - expect(service.prop_updated?(:bamboo_url)).to be_truthy + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_changed?).to be_truthy + end + + it "returns true when the property has been assigned a different value twice" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_changed?).to be_truthy + end + + it "returns false when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_changed?).to be_falsy + end + + it "returns false when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save + expect(service.bamboo_url_changed?).to be_falsy + end + end + + describe "{property}_touched?" do + let(:service) do + BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.bamboo_url_touched?).to be_falsy + end + + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns true when the property has been assigned a different value twice" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns true when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns false when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save + expect(service.bamboo_url_changed?).to be_falsy + end + end + + describe "{property}_was" do + let(:service) do + BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + + it "returns nil when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.bamboo_url_was).to be_nil + end + + it "returns the previous value when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns initial value when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns initial value when the property has been assigned multiple values" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example2.com" + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns nil when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save + expect(service.bamboo_url_was).to be_nil end end end From 97e02f4bc99d391c5e1dc0ce8e317be105e6d2b0 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 15 Oct 2015 12:15:18 +0200 Subject: [PATCH 109/443] Added documentation on the various profiling tools --- doc/development/profiling.md | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 doc/development/profiling.md diff --git a/doc/development/profiling.md b/doc/development/profiling.md new file mode 100644 index 0000000000..80c86ef921 --- /dev/null +++ b/doc/development/profiling.md @@ -0,0 +1,56 @@ +# Profiling + +To make it easier to track down performance problems GitLab comes with a set of +profiling tools, some of these are available by default while others need to be +explicitly enabled. + +## rack-mini-profiler + +This Gem is enabled by default in development only. It allows you to see the +timings of the various components that made up a web request (e.g. the SQL +queries executed and their execution timings). + +## Bullet + +Bullet is a Gem that can be used to track down N+1 query problems. Because +Bullet adds quite a bit of logging noise it's disabled by default. To enable +Bullet, set the environment variable `ENABLE_BULLET` to a non-empty value before +starting GitLab. For example: + + ENABLE_BULLET=true bundle exec rails s + +Bullet will log query problems to both the Rails log as well as the Chrome +console. + +## ActiveRecord Query Trace + +This Gem adds backtraces for every ActiveRecord query in the Rails console. This +can be useful to track down where a query was executed. Because this Gem adds +quite a bit of noise (5-10 extra lines per ActiveRecord query) it's disabled by +default. To use this Gem you'll need to set `ENABLE_QUERY_TRACE` to a non empty +file before starting GitLab. For example: + + ENABLE_QUERY_TRACE=true bundle exec rails s + +## rack-lineprof + +This is a Gem that can trace the execution time of code on a per line basis. +Because this Gem can add quite a bit of overhead it's disabled by default. To +enable it, set the environment variable `ENABLE_LINEPROF` to a non-empty value. +For example: + + ENABLE_LINEPROF=true bundle exec rails s + +Once enabled you'll need to add a query string parameter to a request to +actually profile code execution. The name of the parameter is `lineprof` and +should be set to a regular expression (minus the starting/ending slash) used to +select what files to profile. To profile all files containing "foo" somewhere in +the path you'd use the following parameter: + + ?lineprof=foo + +Or when filtering for files containing "foo" and "bar" in their path: + + ?lineprof=foo|bar + +Once set the profiling output will be displayed in your terminal. From f7e0357840bb0780ca3ba997aa1e2f4c045f08c7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 15 Oct 2015 13:22:28 +0200 Subject: [PATCH 110/443] Track compatible gitlab-git-http-server version --- GITLAB_GIT_HTTP_SERVER_VERSION | 1 + doc/install/installation.md | 1 + doc/update/7.14-to-8.0.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 GITLAB_GIT_HTTP_SERVER_VERSION diff --git a/GITLAB_GIT_HTTP_SERVER_VERSION b/GITLAB_GIT_HTTP_SERVER_VERSION new file mode 100644 index 0000000000..769ed6ae79 --- /dev/null +++ b/GITLAB_GIT_HTTP_SERVER_VERSION @@ -0,0 +1 @@ +0.2.14 diff --git a/doc/install/installation.md b/doc/install/installation.md index 3c62b11988..acc4e50597 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -325,6 +325,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git cd gitlab-git-http-server + sudo -u git -H git checkout 0.2.14 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 7ad4935e83..305017b704 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -84,6 +84,7 @@ Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git cd gitlab-git-http-server +sudo -u git -H git checkout 0.2.14 sudo -u git -H make ``` From d843cbe08d2af5af0ab6a0270aca25dad993e8ed Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 15 Oct 2015 13:44:28 +0200 Subject: [PATCH 111/443] Sentences end in periods. --- app/views/projects/merge_requests/_show.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index e7ac7a0eaa..eeaa72ed21 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -36,7 +36,8 @@ - if @merge_request.open? && @merge_request.can_be_merged? .light.append-bottom-20 You can also accept this merge request manually using the - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + = succeed '.' do + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits.present? %ul.merge-request-tabs From 999051bc91f02292e28582ce538f0d147c7b665e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 15 Oct 2015 14:59:06 +0200 Subject: [PATCH 112/443] Use gitlab-git-http-server 0.3.0 --- GITLAB_GIT_HTTP_SERVER_VERSION | 2 +- doc/install/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GITLAB_GIT_HTTP_SERVER_VERSION b/GITLAB_GIT_HTTP_SERVER_VERSION index 769ed6ae79..0d91a54c7d 100644 --- a/GITLAB_GIT_HTTP_SERVER_VERSION +++ b/GITLAB_GIT_HTTP_SERVER_VERSION @@ -1 +1 @@ -0.2.14 +0.3.0 diff --git a/doc/install/installation.md b/doc/install/installation.md index acc4e50597..e09890dc29 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -325,7 +325,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git cd gitlab-git-http-server - sudo -u git -H git checkout 0.2.14 + sudo -u git -H git checkout 0.3.0 sudo -u git -H make ### Initialize Database and Activate Advanced Features From 0d09b5fefc635120cf6e4234a401028f815fb326 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 15:49:52 +0200 Subject: [PATCH 113/443] Fix builds view count indicator --- app/controllers/projects/builds_controller.rb | 4 ++-- app/views/projects/builds/index.html.haml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 54c01ddf23..816012762c 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -8,8 +8,7 @@ class Projects::BuildsController < Projects::ApplicationController def index @scope = params[:scope] - @all_builds = project.ci_builds.order('created_at DESC').page(params[:page]).per(30) - + @all_builds = project.ci_builds @builds = case @scope when 'all' @@ -19,6 +18,7 @@ class Projects::BuildsController < Projects::ApplicationController else @all_builds.running_or_pending end + @builds = @builds.order('created_at DESC').page(params[:page]).per(30) end def cancel_all diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index b04784025f..4d8ca16d98 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -12,17 +12,17 @@ %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do Running - %span.badge.js-running-count= @all_builds.running_or_pending.size + %span.badge.js-running-count= @all_builds.running_or_pending.count(:id) %li{class: ('active' if @scope == 'finished')} = link_to project_builds_path(@project, scope: :finished) do Finished - %span.badge.js-running-count= @all_builds.finished.size + %span.badge.js-running-count= @all_builds.finished.count(:id) %li{class: ('active' if @scope == 'all')} = link_to project_builds_path(@project, scope: :all) do All - %span.badge.js-totalbuilds-count= @all_builds.size + %span.badge.js-totalbuilds-count= @all_builds.count(:id) .gray-content-block List of #{@scope || 'running'} builds from this project From 71d0cfcff62e1e86401a10233d78c032a793a008 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 17:13:36 +0200 Subject: [PATCH 114/443] Make the builds view and warning notice nicer --- app/helpers/runners_helper.rb | 2 +- app/views/projects/builds/_build.html.haml | 3 +++ app/views/projects/builds/show.html.haml | 8 +++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index bf551778cb..46eb82a354 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -15,7 +15,7 @@ module RunnersHelper end def runner_link(runner) - display_name = truncate(runner.display_name, length: 20) + display_name = truncate(runner.display_name, length: 15) id = "\##{runner.id}" if current_user && current_user.admin diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml index ff146f7389..4ce4ed63b4 100644 --- a/app/views/projects/builds/_build.html.haml +++ b/app/views/projects/builds/_build.html.haml @@ -9,6 +9,9 @@ - else %strong Build ##{build.id} + - if build.show_warning? + %i.fa.fa-warning.text-warning + %td = link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 91c1b16c9f..138dcddb8e 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -44,16 +44,14 @@ .bs-callout.bs-callout-warning %p - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have runners assigned. + This build is stuck because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? - This build is stuck. - %br - This build is stuck, because you don't have any active runners online with these tags assigned to the project: + This build is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| %span.label.label-primary = tag - else - This build is stuck, because you don't have any active runners online that can run this build. + This build is stuck, because you don't have any active runners that can run this build. %br Go to From c82755f539fdcea08bb668d2b5abb4304787c96e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 15 Oct 2015 18:01:24 +0200 Subject: [PATCH 115/443] Update redcarpet gem. Fixes gem memory leak Signed-off-by: Dmitriy Zaporozhets --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 4fbd37f6c8..9254ce2ccf 100644 --- a/Gemfile +++ b/Gemfile @@ -94,7 +94,7 @@ gem "seed-fu", '~> 2.3.5' gem 'html-pipeline', '~> 1.11.0' gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' gem 'github-markup', '~> 1.3.1' -gem 'redcarpet', '~> 3.3.2' +gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' gem 'org-ruby', '~> 0.9.12' diff --git a/Gemfile.lock b/Gemfile.lock index b92d9766d3..53122898b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -539,7 +539,7 @@ GEM trollop rdoc (3.12.2) json (~> 1.4) - redcarpet (3.3.2) + redcarpet (3.3.3) redis (3.2.1) redis-actionpack (4.0.0) actionpack (~> 4) @@ -881,7 +881,7 @@ DEPENDENCIES rails (= 4.1.12) raphael-rails (~> 2.1.2) rdoc (~> 3.6) - redcarpet (~> 3.3.2) + redcarpet (~> 3.3.3) redis-rails (~> 4.0.0) request_store (~> 1.2.0) rerun (~> 0.10.0) From 567460d16dd721e52b164828e854cb8ac87fdf43 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 18:11:46 +0200 Subject: [PATCH 116/443] Added missing comma [ci skip] --- app/views/projects/builds/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 138dcddb8e..c45bfb27b8 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -44,7 +44,7 @@ .bs-callout.bs-callout-warning %p - if no_runners_for_project?(@build.project) - This build is stuck because the project doesn't have any runners online assigned to it. + This build is stuck, because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? This build is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| From 9fd48229860636fecc07d3dde7cb4fe7624ce8f9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 15 Oct 2015 19:02:29 +0200 Subject: [PATCH 117/443] Show last commit from default branch on project home page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/blocks.scss | 5 ++++ app/assets/stylesheets/pages/projects.scss | 29 ++++++++++++++++++++ app/views/projects/_last_commit.html.haml | 12 ++++++++ app/views/projects/show.html.haml | 4 +++ 4 files changed, 50 insertions(+) create mode 100644 app/views/projects/_last_commit.html.haml diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 6ce34b5c3e..32d219d4d6 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -18,6 +18,7 @@ line-height: 36px; } +.content-block, .gray-content-block { margin: -$gl-padding; background-color: $background-color; @@ -27,6 +28,10 @@ border-bottom: 1px solid $border-color; color: $gl-gray; + &.white { + background-color: white; + } + &.top-block { border-top: none; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f7a2284900..b6d53acd11 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -511,3 +511,32 @@ pre.light-well { margin-top: -1px; } } + +.project-last-commit { + .ci-status { + margin-right: 16px; + } + + .commit-row-message { + color: $gl-gray; + } + + .commit_short_id { + margin-left: 5px; + color: $gl-link-color; + font-weight: 600; + } + + .commit-author-link { + margin-left: 7px; + text-decoration: none; + .avatar { + float: none; + margin-right: 4px; + } + + .commit-author-name { + font-weight: 600; + } + } +} diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml new file mode 100644 index 0000000000..0c16c3ccbf --- /dev/null +++ b/app/views/projects/_last_commit.html.haml @@ -0,0 +1,12 @@ +.project-last-commit + - ci_commit = project.ci_commit(commit.sha) + - if ci_commit + = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do + = ci_status_icon(ci_commit) + = ci_commit.status + + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" + · + #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by + = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e95d987d74..e20b1fc49c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -64,6 +64,10 @@ = icon("exclamation-triangle fw") Archived project! Repository is read-only +- if @repository.commit + .content-block.second-block.white + = render 'projects/last_commit', commit: @repository.commit, project: @project + %section - if prefer_readme? .project-show-readme From 6348f654cdd76b062c18ae1bce3eecf0b6dd0c6c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 15:59:06 -0400 Subject: [PATCH 118/443] Update Installation doc for 8-1-stable --- doc/install/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 3c62b11988..b154d28047 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -211,9 +211,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 8-0-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-1-stable gitlab -**Note:** You can change `8-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It From facf8826daae0c5bad9854bb903101dadc04acb8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 16:06:34 -0400 Subject: [PATCH 119/443] Add 8.0-to-8.1 update guide --- doc/update/8.0-to-8.1.md | 228 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 doc/update/8.0-to-8.1.md diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md new file mode 100644 index 0000000000..34d7c25a46 --- /dev/null +++ b/doc/update/8.0-to-8.1.md @@ -0,0 +1,228 @@ +# From 8.0 to 8.1 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version: + +```sh +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +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 8-1-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-1-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.5 +``` + +### 5. Install gitlab-git-http-server + +First we download Go 1.5 and install it into `/usr/local/go`: + +```bash +curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz +echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ + sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.5.linux-amd64.tar.gz +``` + +Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git-http-server`: + +```bash +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git +cd gitlab-git-http-server +sudo -u git -H make +``` + +Make sure your unicorn.rb file contains a 'listen' line for +'127.0.0.1:8080' and that this line is not commented out. + +``` +cd /home/git/gitlab +grep ^listen config/unicorn.rb + +# If there is no 'listen' line for 127.0.0.1:8080, add it: +sudo -u git tee -a config/unicorn.rb < true +EOF +``` + +If your Git repositories are in a directory other than `/home/git/repositories`, +you need to tell `gitlab-git-http-server` about it via `/etc/default/gitlab`. +See `lib/support/init.d/gitlab.default.example` for the options. + +### 6. Copy secrets + +The `secrets.yml` file is used to store keys to encrypt sessions and encrypt secure variables. +When you run migrations make sure to store it someplace safe. +Don't store it in the same place as your database backups, +otherwise your secrets are exposed if one of your backups is compromised. + +``` +cd /home/git/gitlab +sudo -u git -H cp config/secrets.yml.example config/secrets.yml +sudo -u git -H chmod 0600 config/secrets.yml +``` + +### 7. 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 postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --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 +``` + +### 8. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example +``` + +The new options include configuration of GitLab CI that are now being part of GitLab CE and EE. + +#### New Nginx configuration + +Because of the new `gitlab-git-http-server` you need to update your Nginx +configuration. If you skip this step 'git clone' and 'git push' over HTTP(S) +will stop working. + +View changes between the previous recommended Nginx configuration and the +current one: + +```sh +# For HTTPS configurations +git diff origin/8-0-stable:lib/support/nginx/gitlab-ssl origin/8-1-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-0-stable:lib/support/nginx/gitlab origin/8-1-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache). +Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34). + +### 9. Migrate GitLab CI to GitLab CE/EE + +Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually. +Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE. + +### 10. Use Redis v2.4.0+ + +Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but +Sidekiq jobs could fail due to lack of support for the SREM command. GitLab +8.0 now checks that Redis >= 2.4.0 is used. You can check your Redis version +with the following command: + + redis-cli info | grep redis_version + +### 11. Start application + + sudo service gitlab start + sudo service nginx restart + +### 12. 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: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.0) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 7.14 to 8.0](7.14-to-8.0.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. + +## Troubleshooting + +### "You appear to have cloned an empty repository." + +If you see this message when attempting to clone a repository hosted by GitLab, +this is likely due to an outdated Nginx or Apache configuration, or a missing or +misconfigured `gitlab-git-http-server` instance. Double-check that you correctly +completed [Step 5](#5-install-gitlab-git-http-server) to install the daemon and +[Step 8](#new-nginx-configuration) to reconfigure Nginx. From 0aa6061d6ab0ab921ad585329b43b84d20da873e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 15:08:31 +0200 Subject: [PATCH 120/443] Implement when syntax in .gitlab-ci.yml --- CHANGELOG | 1 + app/models/ci/build.rb | 5 +- app/models/ci/commit.rb | 52 ++--- app/models/commit_status.rb | 2 +- app/services/ci/create_builds_service.rb | 14 +- doc/ci/yaml/README.md | 49 ++++ lib/ci/gitlab_ci_yaml_processor.rb | 7 +- lib/ci/status.rb | 21 ++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 28 ++- spec/models/ci/commit_spec.rb | 224 ++++++++++++------- 10 files changed, 279 insertions(+), 124 deletions(-) create mode 100644 lib/ci/status.rb diff --git a/CHANGELOG b/CHANGELOG index aba823948a..3992669214 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.1.0 (unreleased) - Add first and last to pagination (Zeger-Jan van de Weg) - Added Commit Status API - Added Builds View + - Added when to .gitlab-ci.yml - Show CI status on commit page - Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds - Show CI status on Your projects page and Starred projects page diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5f8d44148c..b19e2ac136 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -93,10 +93,7 @@ module Ci Ci::WebHookService.new.build_end(build) end - if build.commit.should_create_next_builds?(build) - build.commit.create_next_builds(build.ref, build.tag, build.user, build.trigger_request) - end - + build.commit.create_next_builds(build) project.execute_services(build) if project.coverage_enabled? diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index cd45366b34..13437b2483 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -91,19 +91,28 @@ module Ci def create_builds(ref, tag, user, trigger_request = nil) return unless config_processor config_processor.stages.any? do |stage| - CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? + CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present? end end - def create_next_builds(ref, tag, user, trigger_request) + def create_next_builds(build) return unless config_processor - stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage) + # don't create other builds if this one is retried + latest_builds = builds.similar(build).latest + return unless latest_builds.exists?(build.id) - config_processor.stages.any? do |stage| - unless stages.include?(stage) - CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? - end + # get list of stages after this build + next_stages = config_processor.stages.drop_while { |stage| stage != build.stage } + next_stages.delete(build.stage) + + # get status for all prior builds + prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) } + status = Ci::Status.get_status(prior_builds) + + # create builds for next stages based + next_stages.any? do |stage| + CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present? end end @@ -132,24 +141,7 @@ module Ci return 'failed' end - @status ||= begin - latest = latest_statuses - latest.reject! { |status| status.try(&:allow_failure?) } - - if latest.none? - 'skipped' - elsif latest.all?(&:success?) - 'success' - elsif latest.all?(&:pending?) - 'pending' - elsif latest.any?(&:running?) || latest.any?(&:pending?) - 'running' - elsif latest.all?(&:canceled?) - 'canceled' - else - 'failed' - end - end + @status ||= Ci::Status.get_status(latest_statuses) end def pending? @@ -219,16 +211,6 @@ module Ci update!(committed_at: DateTime.now) end - def should_create_next_builds?(build) - # don't create other builds if this one is retried - other_builds = builds.similar(build).latest - return false unless other_builds.include?(build) - - other_builds.all? do |build| - build.success? || build.ignored? - end - end - private def save_yaml_error(error) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0b71838d51..8188ba3a28 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -28,7 +28,7 @@ class CommitStatus < ActiveRecord::Base end event :drop do - transition running: :failed + transition [:pending, :running] => :failed end event :success do diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index c420f3268f..912eb6258a 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,8 +1,20 @@ module Ci class CreateBuildsService - def execute(commit, stage, ref, tag, user, trigger_request) + def execute(commit, stage, ref, tag, user, trigger_request, status) builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag) + # check when to create next build + builds_attrs = builds_attrs.select do |build_attrs| + case build_attrs[:when] + when 'on_success' + status == 'success' + when 'on_failure' + status == 'failed' + when 'always' + %w(success failed).include?(status) + end + end + builds_attrs.map do |build_attrs| # don't create the same build twice unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4caeccacb7..8507389f1c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -140,6 +140,7 @@ job_name: | except | optional | Defines a list of git refs for which build is not created | | tags | optional | Defines a list of tags which are used to select runner | | allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | +| when | optional | Define when to run build. Can be on_success, on_failure or always | ### script `script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. @@ -196,6 +197,54 @@ job: The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined. +### when +`when` is used to implement jobs that are run in case of failure or despite the failure. + +The `when` can be set to one of the following values: +1. `on_success` - execute build only when all builds from prior stages succeeded. This is default. +1. `on_failure` - execute build only when at least one of the build from prior stages failed. +1. `always` - execute build despite the status of builds from prior stages. + +``` +stages: +- build +- cleanup_build +- test +- deploy +- cleanup + +build: + stage: build + script: + - make build + +cleanup_build: + stage: cleanup_build + script: + - cleanup build when failed + when: on_failure + +test: + stage: test + script: + - make test + +deploy: + stage: deploy + script: + - make deploy + +cleanup: + stage: cleanup + script: + - cleanup after builds + when: always +``` + +The above script will: +1. Execute `cleanup_build` only when the `build` failed, +2. Always execute `cleanup` as the last step in pipeline. + ## Validate the .gitlab-ci.yml Each instance of GitLab CI has an embedded debug tool called Lint. You can find the link to the Lint in the project's settings page or use short url `/lint`. diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index c47951bc5d..58be188387 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -5,7 +5,7 @@ module Ci DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables] - ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage] + ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when] attr_reader :before_script, :image, :services, :variables @@ -93,6 +93,7 @@ module Ci only: job[:only], except: job[:except], allow_failure: job[:allow_failure] || false, + when: job[:when] || 'on_success', options: { image: job[:image] || @image, services: job[:services] || @services @@ -184,6 +185,10 @@ module Ci if job[:allow_failure] && !job[:allow_failure].in?([true, false]) raise ValidationError, "#{name}: allow_failure parameter should be an boolean" end + + if job[:when] && !job[:when].in?(%w(on_success on_failure always)) + raise ValidationError, "#{name}: when should be on_success, on_failure or always" + end end private diff --git a/lib/ci/status.rb b/lib/ci/status.rb new file mode 100644 index 0000000000..94c94261d8 --- /dev/null +++ b/lib/ci/status.rb @@ -0,0 +1,21 @@ +module Ci + class Status + def self.get_status(statuses) + statuses.reject! { |status| status.try(&:allow_failure?) } + + if statuses.none? + 'skipped' + elsif statuses.all?(&:success?) + 'success' + elsif statuses.all?(&:pending?) + 'pending' + elsif statuses.any?(&:running?) || statuses.any?(&:pending?) + 'running' + elsif statuses.all?(&:canceled?) + 'canceled' + else + 'failed' + end + end + end +end \ No newline at end of file diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index aba957da48..65696cb1ed 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -125,7 +125,8 @@ module Ci image: "ruby:2.1", services: ["mysql"] }, - allow_failure: false + allow_failure: false, + when: "on_success" }) end @@ -152,7 +153,8 @@ module Ci image: "ruby:2.5", services: ["postgresql"] }, - allow_failure: false + allow_failure: false, + when: "on_success" }) end end @@ -174,6 +176,21 @@ module Ci end end + describe "When" do + %w(on_success on_failure always).each do |when_state| + it "returns #{when_state} when defined" do + config = YAML.dump({ + rspec: { script: "rspec", when: when_state } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + builds = config_processor.builds_for_stage_and_ref("test", "master") + expect(builds.size).to eq(1) + expect(builds.first[:when]).to eq(when_state) + end + end + end + describe "Error handling" do it "indicates that object is invalid" do expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) @@ -311,6 +328,13 @@ module Ci GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") end + + it "returns errors if job when is not on_success, on_failure or always" do + config = YAML.dump({ rspec: { script: "test", when: false } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") + end end end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index d1cecce5a6..9ad3040776 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -161,28 +161,28 @@ describe Ci::Commit do end describe :create_builds do - let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } def create_builds(trigger_request = nil) commit.create_builds('master', false, nil, trigger_request) end - def create_next_builds(trigger_request = nil) - commit.create_next_builds('master', false, nil, trigger_request) + def create_next_builds + commit.create_next_builds(commit.builds.order(:id).last) end it 'creates builds' do expect(create_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) + commit.builds.update_all(status: "success") + expect(commit.builds.count(:all)).to eq(2) expect(create_next_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(4) + commit.builds.update_all(status: "success") + expect(commit.builds.count(:all)).to eq(4) expect(create_next_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(5) + commit.builds.update_all(status: "success") + expect(commit.builds.count(:all)).to eq(5) expect(create_next_builds).to be_falsey end @@ -194,12 +194,12 @@ describe Ci::Commit do it 'creates builds' do expect(create_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) + commit.builds.update_all(status: "success") + expect(commit.builds.count(:all)).to eq(2) expect(create_develop_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(4) + commit.builds.update_all(status: "success") + expect(commit.builds.count(:all)).to eq(4) expect(commit.refs.size).to eq(2) expect(commit.builds.pluck(:name).uniq.size).to eq(2) end @@ -211,28 +211,24 @@ describe Ci::Commit do it 'creates builds' do expect(create_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) + expect(commit.builds.count(:all)).to eq(2) end it 'rebuilds commit' do expect(create_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) + expect(commit.builds.count(:all)).to eq(2) expect(create_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(4) + expect(commit.builds.count(:all)).to eq(4) end it 'creates next builds' do expect(create_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) + expect(commit.builds.count(:all)).to eq(2) + commit.builds.update_all(status: "success") - expect(create_next_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(4) + expect(create_next_builds).to be_truthy + expect(commit.builds.count(:all)).to eq(4) end context 'for [ci skip]' do @@ -242,7 +238,7 @@ describe Ci::Commit do it 'rebuilds commit' do expect(commit.status).to eq('skipped') - expect(create_builds(trigger_request)).to be_truthy + expect(create_builds).to be_truthy # since everything in Ci::Commit is cached we need to fetch a new object new_commit = Ci::Commit.find_by_id(commit.id) @@ -250,6 +246,129 @@ describe Ci::Commit do end end end + + context 'properly creates builds "when" is defined' do + let(:yaml) { + { + stages: ["build", "test", "test_failure", "deploy", "cleanup"], + build: { + stage: "build", + script: "BUILD", + }, + test: { + stage: "test", + script: "TEST", + }, + test_failure: { + stage: "test_failure", + script: "ON test failure", + when: "on_failure", + }, + deploy: { + stage: "deploy", + script: "PUBLISH", + }, + cleanup: { + stage: "cleanup", + script: "TIDY UP", + when: "always", + } + } + } + + before do + stub_ci_commit_yaml_file(YAML.dump(yaml)) + end + + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + expect(commit.status).to eq('success') + end + + it 'properly creates builds when test fails' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + expect(commit.status).to eq('failed') + end + + it 'properly creates builds when test and test_failure fails' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + expect(commit.status).to eq('failed') + end + + it 'properly creates builds when deploy fails' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + expect(commit.status).to eq('failed') + end + end end describe "#finished_at" do @@ -299,59 +418,4 @@ describe Ci::Commit do expect(commit.coverage).to be_nil end end - - describe :should_create_next_builds? do - before do - @build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: 'success' - @build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: 'failed' - @build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: 'failed' - @build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'success' - end - - context 'for success' do - it 'to create if all succeeded' do - expect(commit.should_create_next_builds?(@build4)).to be_truthy - end - end - - context 'for failed' do - before do - @build4.update_attributes(status: 'failed') - end - - it 'to not create' do - expect(commit.should_create_next_builds?(@build4)).to be_falsey - end - - context 'and ignore failures for current' do - before do - @build4.update_attributes(allow_failure: true) - end - - it 'to create' do - expect(commit.should_create_next_builds?(@build4)).to be_truthy - end - end - end - - context 'for running' do - before do - @build4.update_attributes(status: 'running') - end - - it 'to not create' do - expect(commit.should_create_next_builds?(@build4)).to be_falsey - end - end - - context 'for retried' do - before do - @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'failed' - end - - it 'to not create' do - expect(commit.should_create_next_builds?(@build4)).to be_falsey - end - end - end end From 9419046196dc1a09716d5b2bbc424b4f89d696ae Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 23:49:27 +0200 Subject: [PATCH 121/443] Fix specs --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 5 +++-- spec/models/ci/commit_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 58be188387..0da73e387e 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -187,7 +187,7 @@ module Ci end if job[:when] && !job[:when].in?(%w(on_success on_failure always)) - raise ValidationError, "#{name}: when should be on_success, on_failure or always" + raise ValidationError, "#{name}: when parameter should be on_success, on_failure or always" end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 65696cb1ed..2260a6f813 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -24,7 +24,8 @@ module Ci commands: "pwd\nrspec", tag_list: [], options: {}, - allow_failure: false + allow_failure: false, + when: "on_success" }) end @@ -330,7 +331,7 @@ module Ci end it "returns errors if job when is not on_success, on_failure or always" do - config = YAML.dump({ rspec: { script: "test", when: false } }) + config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 9ad3040776..94fc21b4ea 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -248,7 +248,7 @@ describe Ci::Commit do end context 'properly creates builds "when" is defined' do - let(:yaml) { + let(:yaml) do { stages: ["build", "test", "test_failure", "deploy", "cleanup"], build: { @@ -274,7 +274,7 @@ describe Ci::Commit do when: "always", } } - } + end before do stub_ci_commit_yaml_file(YAML.dump(yaml)) From 374ea21f32f2977533be2c9b124867bf7c72407c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Oct 2015 23:56:42 +0200 Subject: [PATCH 122/443] Backticks around when types --- doc/ci/yaml/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 8507389f1c..d37e616bba 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -140,7 +140,7 @@ job_name: | except | optional | Defines a list of git refs for which build is not created | | tags | optional | Defines a list of tags which are used to select runner | | allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | -| when | optional | Define when to run build. Can be on_success, on_failure or always | +| when | optional | Define when to run build. Can be `on_success`, `on_failure` or `always` | ### script `script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. From 6232bb1ef39c310f5cd7fb6d11d75b9d51e55e58 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 18:18:41 -0400 Subject: [PATCH 123/443] Remove notes about CI migration from 8.1 update guide [ci skip] --- doc/update/8.0-to-8.1.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md index 34d7c25a46..43c06609f4 100644 --- a/doc/update/8.0-to-8.1.md +++ b/doc/update/8.0-to-8.1.md @@ -148,8 +148,6 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example ``` -The new options include configuration of GitLab CI that are now being part of GitLab CE and EE. - #### New Nginx configuration Because of the new `gitlab-git-http-server` you need to update your Nginx @@ -170,12 +168,7 @@ git diff origin/8-0-stable:lib/support/nginx/gitlab origin/8-1-stable:lib/suppor If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache). Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34). -### 9. Migrate GitLab CI to GitLab CE/EE - -Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually. -Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE. - -### 10. Use Redis v2.4.0+ +### 9. Use Redis v2.4.0+ Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but Sidekiq jobs could fail due to lack of support for the SREM command. GitLab @@ -184,12 +177,12 @@ with the following command: redis-cli info | grep redis_version -### 11. Start application +### 10. Start application sudo service gitlab start sudo service nginx restart -### 12. Check application status +### 11. Check application status Check if GitLab and its environment are configured correctly: From c2c9f6d52d392be8bb6bce5366cdcbcfdf38c9e5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 18:49:37 -0400 Subject: [PATCH 124/443] Remove 8.0-only steps from the 8.1 update guide Also adds a preamble note that a working 8.0 installation (i.e., gitlab-git-http-server, updated Nginx/Apache configs) is required before proceeding. [ci skip] --- doc/update/8.0-to-8.1.md | 98 ++++------------------------------------ 1 file changed, 9 insertions(+), 89 deletions(-) diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md index 43c06609f4..4dacc97f7f 100644 --- a/doc/update/8.0-to-8.1.md +++ b/doc/update/8.0-to-8.1.md @@ -1,5 +1,9 @@ # From 8.0 to 8.1 +**NOTE:** GitLab 8.0 introduced several significant changes related to +installation and configuration which *are not duplicated here*. Be sure you're +already running a working version of 8.0 before proceeding with this guide. + ### 0. Double-check your Git version **This notice applies only to /usr/local/bin/git** @@ -66,58 +70,7 @@ sudo -u git -H git fetch sudo -u git -H git checkout v2.6.5 ``` -### 5. Install gitlab-git-http-server - -First we download Go 1.5 and install it into `/usr/local/go`: - -```bash -curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz -echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ - sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz -sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ -rm go1.5.linux-amd64.tar.gz -``` - -Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git-http-server`: - -```bash -cd /home/git -sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git -cd gitlab-git-http-server -sudo -u git -H make -``` - -Make sure your unicorn.rb file contains a 'listen' line for -'127.0.0.1:8080' and that this line is not commented out. - -``` -cd /home/git/gitlab -grep ^listen config/unicorn.rb - -# If there is no 'listen' line for 127.0.0.1:8080, add it: -sudo -u git tee -a config/unicorn.rb < true -EOF -``` - -If your Git repositories are in a directory other than `/home/git/repositories`, -you need to tell `gitlab-git-http-server` about it via `/etc/default/gitlab`. -See `lib/support/init.d/gitlab.default.example` for the options. - -### 6. Copy secrets - -The `secrets.yml` file is used to store keys to encrypt sessions and encrypt secure variables. -When you run migrations make sure to store it someplace safe. -Don't store it in the same place as your database backups, -otherwise your secrets are exposed if one of your backups is compromised. - -``` -cd /home/git/gitlab -sudo -u git -H cp config/secrets.yml.example config/secrets.yml -sudo -u git -H chmod 0600 config/secrets.yml -``` - -### 7. Install libs, migrations, etc. +### 5. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -138,7 +91,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab ``` -### 8. Update config files +### 6. Update configuration files #### New configuration options for `gitlab.yml` @@ -148,41 +101,12 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example ``` -#### New Nginx configuration - -Because of the new `gitlab-git-http-server` you need to update your Nginx -configuration. If you skip this step 'git clone' and 'git push' over HTTP(S) -will stop working. - -View changes between the previous recommended Nginx configuration and the -current one: - -```sh -# For HTTPS configurations -git diff origin/8-0-stable:lib/support/nginx/gitlab-ssl origin/8-1-stable:lib/support/nginx/gitlab-ssl - -# For HTTP configurations -git diff origin/8-0-stable:lib/support/nginx/gitlab origin/8-1-stable:lib/support/nginx/gitlab -``` - -If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache). -Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34). - -### 9. Use Redis v2.4.0+ - -Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but -Sidekiq jobs could fail due to lack of support for the SREM command. GitLab -8.0 now checks that Redis >= 2.4.0 is used. You can check your Redis version -with the following command: - - redis-cli info | grep redis_version - -### 10. Start application +### 7. Start application sudo service gitlab start sudo service nginx restart -### 11. Check application status +### 8. Check application status Check if GitLab and its environment are configured correctly: @@ -214,8 +138,4 @@ If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of ### "You appear to have cloned an empty repository." -If you see this message when attempting to clone a repository hosted by GitLab, -this is likely due to an outdated Nginx or Apache configuration, or a missing or -misconfigured `gitlab-git-http-server` instance. Double-check that you correctly -completed [Step 5](#5-install-gitlab-git-http-server) to install the daemon and -[Step 8](#new-nginx-configuration) to reconfigure Nginx. +See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting). From 62377d17548ca41b4c563ec1c7331df97f1054ca Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 19:44:15 -0400 Subject: [PATCH 125/443] Shut up, Rubocop --- lib/ci/status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ci/status.rb b/lib/ci/status.rb index 94c94261d8..c02b3b8f3e 100644 --- a/lib/ci/status.rb +++ b/lib/ci/status.rb @@ -18,4 +18,4 @@ module Ci end end end -end \ No newline at end of file +end From 6fe2a679a799c0914b8c32e011343939800c5480 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 19:46:01 -0400 Subject: [PATCH 126/443] Update CI YAML docs --- doc/ci/yaml/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d37e616bba..ea8f72bc13 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -200,9 +200,10 @@ The above specification will make sure that `job` is built by a runner that have ### when `when` is used to implement jobs that are run in case of failure or despite the failure. -The `when` can be set to one of the following values: -1. `on_success` - execute build only when all builds from prior stages succeeded. This is default. -1. `on_failure` - execute build only when at least one of the build from prior stages failed. +`when` can be set to one of the following values: + +1. `on_success` - execute build only when all builds from prior stages succeeded. This is the default. +1. `on_failure` - execute build only when at least one build from prior stages failed. 1. `always` - execute build despite the status of builds from prior stages. ``` @@ -250,4 +251,4 @@ Each instance of GitLab CI has an embedded debug tool called Lint. You can find the link to the Lint in the project's settings page or use short url `/lint`. ## Skipping builds -There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped \ No newline at end of file +There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped From 64352d25b33274a2f298347bae5f53176085b80d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 15 Oct 2015 21:30:47 -0400 Subject: [PATCH 127/443] Correct spec description typo [ci skip] --- spec/models/ci/commit_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 94fc21b4ea..44dbd083f0 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -247,7 +247,7 @@ describe Ci::Commit do end end - context 'properly creates builds "when" is defined' do + context 'properly creates builds when "when" is defined' do let(:yaml) do { stages: ["build", "test", "test_failure", "deploy", "cleanup"], From a317c5296e9c142cfd04168eced7d7bfd9458ffa Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Fri, 16 Oct 2015 07:36:20 +0100 Subject: [PATCH 128/443] Changed loose to lose --- doc/raketasks/backup_restore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index db3f6bb40b..06f582dcee 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -16,7 +16,7 @@ You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` from source). This file contains the database encryption key used for two-factor authentication. If you restore a GitLab backup without restoring the database encryption key, users who have two-factor -authentication enabled will loose access to your GitLab server. +authentication enabled will lose access to your GitLab server. If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* From 2611d9f63cb6c22a00eccdac75535f93890b176c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 01:41:46 -0700 Subject: [PATCH 129/443] Add a system note and update relevant merge requests when a branch is deleted or re-added If a branch is deleted with an open merge request, amended offline, and then pushed again, GitLab doesn't bother to update the merge request even though the last commit ID and/or code may have changed. This MR ensures that each push will update any relevant merge requests and adds a system note if this happens as well. Closes #2926 --- CHANGELOG | 1 + app/models/merge_request_diff.rb | 3 +- app/services/git_push_service.rb | 5 +- .../merge_requests/refresh_service.rb | 55 +++++++++++-------- app/services/system_note_service.rb | 19 +++++++ spec/services/git_push_service_spec.rb | 8 +++ .../merge_requests/refresh_service_spec.rb | 19 +++++++ spec/services/system_note_service_spec.rb | 12 ++++ 8 files changed, 98 insertions(+), 24 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3992669214..814a6772cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x + - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) - Improved performance of finding users by username or Email address - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index c9ef8023ae..bc2d691ece 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -163,7 +163,8 @@ class MergeRequestDiff < ActiveRecord::Base merge_request.fetch_ref # Get latest sha of branch from source project - source_sha = merge_request.source_project.commit(source_branch).sha + source_commit = merge_request.source_project.commit(source_branch) + source_sha = source_commit.try(:sha) Gitlab::CompareResult.new( Gitlab::Git::Compare.new( diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 81d47602f1..e54044365b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -49,10 +49,13 @@ class GitPushService elsif push_to_existing_branch?(ref, oldrev) # Collect data for this git push @push_commits = project.repository.commits_between(oldrev, newrev) - project.update_merge_requests(oldrev, newrev, ref, @user) process_commit_messages(ref) end + # Update merge requests that may be affected by this push. A new branch + # could cause the last commit of a merge request to change. + project.update_merge_requests(oldrev, newrev, ref, @user) + @push_data = build_push_data(oldrev, newrev, ref) # If CI was disabled but .gitlab-ci.yml file was pushed diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e903e48e3c..802d02b079 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -6,12 +6,22 @@ module MergeRequests @oldrev, @newrev = oldrev, newrev @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened - @commits = @project.repository.commits_between(oldrev, newrev) + @commits = [] + @merge_requests = merge_requests_for_branch + + # Leave a system note if a branch were deleted/added + if Gitlab::Git.blank_ref?(oldrev) or Gitlab::Git.blank_ref?(newrev) + presence = Gitlab::Git.blank_ref?(oldrev) ? 'added' : 'deleted' + comment_mr_branch_presence_changed(presence) + else + @commits = @project.repository.commits_between(oldrev, newrev) + + close_merge_requests + comment_mr_with_commits + end - close_merge_requests reload_merge_requests execute_mr_web_hooks - comment_mr_with_commits true end @@ -31,7 +41,6 @@ module MergeRequests commit_ids.include?(merge_request.last_commit.id) end - merge_requests.uniq.select(&:source_project).each do |merge_request| MergeRequests::PostMergeService. new(merge_request.target_project, @current_user). @@ -46,11 +55,7 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a - merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a - merge_requests = filter_merge_requests(merge_requests) - - merge_requests.each do |merge_request| + @merge_requests.each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code @@ -70,13 +75,20 @@ module MergeRequests end end - # Add comment about pushing new commits to merge requests - def comment_mr_with_commits - merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a - merge_requests = filter_merge_requests(merge_requests) + # Add comment about branches being deleted or added to merge requests + def comment_mr_branch_presence_changed(presence) + merge_requests = merge_requests_for_branch merge_requests.each do |merge_request| + SystemNoteService.change_branch_presence( + merge_request, merge_request.project, @current_user, + 'source', @branch_name, presence) + end + end + + # Add comment about pushing new commits to merge requests + def comment_mr_with_commits + @merge_requests.each do |merge_request| mr_commit_ids = Set.new(merge_request.commits.map(&:id)) new_commits, existing_commits = @commits.partition do |commit| @@ -91,14 +103,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks - merge_requests = @project.origin_merge_requests.opened - .where(source_branch: @branch_name) - .to_a - merge_requests += @fork_merge_requests.where(source_branch: @branch_name) - .to_a - merge_requests = filter_merge_requests(merge_requests) - - merge_requests.each do |merge_request| + @merge_requests.each do |merge_request| execute_hooks(merge_request, 'update') end end @@ -106,5 +111,11 @@ module MergeRequests def filter_merge_requests(merge_requests) merge_requests.uniq.select(&:source_project) end + + def merge_requests_for_branch + merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a + filter_merge_requests(merge_requests) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8253c1f780..24e0650407 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -168,6 +168,25 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when a branch in Noteable is added or deleted + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # branch_type - 'source' or 'target' + # branch - branch name + # presence - 'deleted' or 'created' + # + # Example Note text: + # + # "Target branch `feature` deleted" + # + # Returns the created Note object + def self.change_branch_presence(noteable, project, author, branch_type, branch, presence) + body = "#{branch_type} branch `#{branch}` #{presence}".capitalize + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when a Mentionable references a Noteable # # noteable - Noteable object being referenced diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index fd72905c33..17015d29e5 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -112,6 +112,14 @@ describe GitPushService do it { expect(@event.project).to eq(project) } it { expect(@event.action).to eq(Event::PUSHED) } it { expect(@event.data).to eq(service.push_data) } + + context "Updates merge requests" do + it "when pushing a new branch for the first time" do + expect(project).to receive(:update_merge_requests). + with(@blankrev, 'newrev', 'refs/heads/master', user) + service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + end + end end describe "Web Hooks" do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 9516e7936d..edb9fd0b43 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -106,6 +106,25 @@ describe MergeRequests::RefreshService do it { expect(@fork_merge_request.notes).to be_empty } end + context 'push new branch that exists in a merge request' do + let(:refresh_service) { service.new(@fork_project, @user) } + before do + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'should execute hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@fork_merge_request, 'update') + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes.last.note).to include('Source branch `master` added') } + it { expect(@fork_merge_request).to be_open } + end + def reload_mrs @merge_request.reload @fork_merge_request.reload diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 2658576640..108bc5995d 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -242,6 +242,18 @@ describe SystemNoteService do end end + describe '.change_branch_presence' do + subject { described_class.change_branch_presence(noteable, project, author, 'source', 'feature', 'deleted') } + + it_behaves_like 'a system note' + + context 'when source branch deleted' do + it 'sets the note text' do + expect(subject.note).to eq "Source branch `feature` deleted" + end + end + end + describe '.cross_reference' do subject { described_class.cross_reference(noteable, mentioner, author) } From 22775c596f9f8be79ec7599b561aa0ccb40bdc42 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 01:57:45 -0700 Subject: [PATCH 130/443] Preserve target branch --- app/services/merge_requests/refresh_service.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 802d02b079..8139aef2bb 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -55,7 +55,11 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - @merge_requests.each do |merge_request| + merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a + merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code From effa94bb878f4e9c208640c0f067b20cc004db2c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 02:08:22 -0700 Subject: [PATCH 131/443] Improve SystemNote interface for branch add/restore case --- app/services/merge_requests/refresh_service.rb | 16 +++++++--------- app/services/system_note_service.rb | 12 +++++++++--- .../merge_requests/refresh_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 4 ++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 8139aef2bb..fd15889343 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -7,11 +7,11 @@ module MergeRequests @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = [] - @merge_requests = merge_requests_for_branch + @source_merge_requests = merge_requests_for_source_branch # Leave a system note if a branch were deleted/added if Gitlab::Git.blank_ref?(oldrev) or Gitlab::Git.blank_ref?(newrev) - presence = Gitlab::Git.blank_ref?(oldrev) ? 'added' : 'deleted' + presence = Gitlab::Git.blank_ref?(oldrev) ? :add : :delete comment_mr_branch_presence_changed(presence) else @commits = @project.repository.commits_between(oldrev, newrev) @@ -81,18 +81,16 @@ module MergeRequests # Add comment about branches being deleted or added to merge requests def comment_mr_branch_presence_changed(presence) - merge_requests = merge_requests_for_branch - - merge_requests.each do |merge_request| + @source_merge_requests.each do |merge_request| SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, - 'source', @branch_name, presence) + :source, @branch_name, presence) end end # Add comment about pushing new commits to merge requests def comment_mr_with_commits - @merge_requests.each do |merge_request| + @source_merge_requests.each do |merge_request| mr_commit_ids = Set.new(merge_request.commits.map(&:id)) new_commits, existing_commits = @commits.partition do |commit| @@ -107,7 +105,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks - @merge_requests.each do |merge_request| + @source_merge_requests.each do |merge_request| execute_hooks(merge_request, 'update') end end @@ -116,7 +114,7 @@ module MergeRequests merge_requests.uniq.select(&:source_project) end - def merge_requests_for_branch + def merge_requests_for_source_branch merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a filter_merge_requests(merge_requests) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 24e0650407..c0a5c3aeb5 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -173,9 +173,9 @@ class SystemNoteService # noteable - Noteable object # project - Project owning noteable # author - User performing the change - # branch_type - 'source' or 'target' + # branch_type - :source or :target # branch - branch name - # presence - 'deleted' or 'created' + # presence - :add or :delete # # Example Note text: # @@ -183,7 +183,13 @@ class SystemNoteService # # Returns the created Note object def self.change_branch_presence(noteable, project, author, branch_type, branch, presence) - body = "#{branch_type} branch `#{branch}` #{presence}".capitalize + verb = + if presence == :add + 'restored' + else + 'deleted' + end + body = "#{branch_type.to_s} branch `#{branch}` #{verb}".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index edb9fd0b43..41eb5d41b2 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -121,7 +121,7 @@ describe MergeRequests::RefreshService do it { expect(@merge_request.notes).to be_empty } it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('Source branch `master` added') } + it { expect(@fork_merge_request.notes.last.note).to include('Source branch `master` restored') } it { expect(@fork_merge_request).to be_open } end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 108bc5995d..16b1c66ff9 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -229,7 +229,7 @@ describe SystemNoteService do end describe '.change_branch' do - subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } + subject { described_class.change_branch(noteable, project, author, :target, old_branch, new_branch) } let(:old_branch) { 'old_branch'} let(:new_branch) { 'new_branch'} @@ -243,7 +243,7 @@ describe SystemNoteService do end describe '.change_branch_presence' do - subject { described_class.change_branch_presence(noteable, project, author, 'source', 'feature', 'deleted') } + subject { described_class.change_branch_presence(noteable, project, author, 'source', 'feature', :delete) } it_behaves_like 'a system note' From 9c67f4fb51719116737f9812d312f20eefebeacd Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 02:24:30 -0700 Subject: [PATCH 132/443] Memoize merge request source branches --- app/services/merge_requests/refresh_service.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index fd15889343..0362b2d0ea 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -7,10 +7,9 @@ module MergeRequests @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = [] - @source_merge_requests = merge_requests_for_source_branch # Leave a system note if a branch were deleted/added - if Gitlab::Git.blank_ref?(oldrev) or Gitlab::Git.blank_ref?(newrev) + if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) presence = Gitlab::Git.blank_ref?(oldrev) ? :add : :delete comment_mr_branch_presence_changed(presence) else @@ -81,7 +80,7 @@ module MergeRequests # Add comment about branches being deleted or added to merge requests def comment_mr_branch_presence_changed(presence) - @source_merge_requests.each do |merge_request| + merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, :source, @branch_name, presence) @@ -90,7 +89,7 @@ module MergeRequests # Add comment about pushing new commits to merge requests def comment_mr_with_commits - @source_merge_requests.each do |merge_request| + merge_requests_for_source_branch.each do |merge_request| mr_commit_ids = Set.new(merge_request.commits.map(&:id)) new_commits, existing_commits = @commits.partition do |commit| @@ -105,7 +104,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks - @source_merge_requests.each do |merge_request| + merge_requests_for_source_branch.each do |merge_request| execute_hooks(merge_request, 'update') end end @@ -115,9 +114,11 @@ module MergeRequests end def merge_requests_for_source_branch - merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a - filter_merge_requests(merge_requests) + @source_merge_requests ||= begin + merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a + filter_merge_requests(merge_requests) + end end end end From 577565d939d60cd48ce9e8aebc44663076af5aa2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 22:45:06 -0700 Subject: [PATCH 133/443] Add system notes for restored branches --- app/models/repository.rb | 4 +++ .../merge_requests/refresh_service.rb | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 8b51602bc2..921e1a9e42 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -480,6 +480,10 @@ class Repository end end + def merge_base(first_commit_id, second_commit_id) + rugged.merge_base(first_commit_id, second_commit_id) + end + def search_files(query, ref) offset = 2 args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref}) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 0362b2d0ea..7c4cb35ec6 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -10,13 +10,12 @@ module MergeRequests # Leave a system note if a branch were deleted/added if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) - presence = Gitlab::Git.blank_ref?(oldrev) ? :add : :delete - comment_mr_branch_presence_changed(presence) + comment_mr_branch_presence_changed + comment_mr_with_commits if @commits.present? else @commits = @project.repository.commits_between(oldrev, newrev) - - close_merge_requests comment_mr_with_commits + close_merge_requests end reload_merge_requests @@ -79,8 +78,29 @@ module MergeRequests end # Add comment about branches being deleted or added to merge requests - def comment_mr_branch_presence_changed(presence) + def comment_mr_branch_presence_changed + presence = Gitlab::Git.blank_ref?(@oldrev) ? :add : :delete + merge_requests_for_source_branch.each do |merge_request| + last_commit = merge_request.last_commit + + # Only look at changed commits in restore branch case + unless Gitlab::Git.blank_ref?(@newrev) + begin + # Since any number of commits could have been made to the restored branch, + # find the common root to see what has been added. + common_ref = @project.repository.merge_base(last_commit.id, @newrev) + # If the last_commit no longer exists in this new branch, + # gitlab_git throws a Rugged::OdbError + # This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 + @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref + rescue => e + end + + # Prevent system notes from seeing a blank SHA + @oldrev = nil + end + SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, :source, @branch_name, presence) From bf290a52b7589ccd0e37a224ec36cec28acfb6a8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 15 Oct 2015 23:19:34 -0700 Subject: [PATCH 134/443] Rubocop fix --- app/services/merge_requests/refresh_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 7c4cb35ec6..c378f14c26 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -94,7 +94,7 @@ module MergeRequests # gitlab_git throws a Rugged::OdbError # This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref - rescue => e + rescue end # Prevent system notes from seeing a blank SHA From 33574c02782824bb1c592a0f783b02dc0f7cca0a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 16 Oct 2015 09:31:35 +0200 Subject: [PATCH 135/443] Fix padding of outdated discussion item. --- CHANGELOG | 1 + app/assets/stylesheets/pages/notes.scss | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3992669214..711d1fcba6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,6 +62,7 @@ v 8.1.0 (unreleased) - Only render 404 page from /public - Hide passwords from services API (Alex Lossent) - Fix: Images cannot show when projects' path was changed + - Fix padding of outdated discussion item. v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index abb03b07f5..1980fe0d45 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -30,7 +30,6 @@ ul.notes { .discussion-header, .note-header { @extend .cgray; - padding-bottom: 15px; a:hover { text-decoration: none; @@ -75,6 +74,10 @@ ul.notes { } } + .discussion-body { + padding-top: 15px; + } + .discussion { overflow: hidden; display: block; From 888c1a3fc53ff0318cd69d0e7f1edad25f306713 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Oct 2015 00:25:19 -0700 Subject: [PATCH 136/443] Add spec for refresh service adding notes to restored branches --- .../merge_requests/refresh_service.rb | 5 ++-- .../merge_requests/refresh_service_spec.rb | 24 ++++++++++--------- spec/services/system_note_service_spec.rb | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index c378f14c26..121f689901 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -90,9 +90,8 @@ module MergeRequests # Since any number of commits could have been made to the restored branch, # find the common root to see what has been added. common_ref = @project.repository.merge_base(last_commit.id, @newrev) - # If the last_commit no longer exists in this new branch, - # gitlab_git throws a Rugged::OdbError - # This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 + # If the a commit no longer exists in this repo, gitlab_git throws + # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref rescue end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 41eb5d41b2..463cd594fb 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -108,21 +108,23 @@ describe MergeRequests::RefreshService do context 'push new branch that exists in a merge request' do let(:refresh_service) { service.new(@fork_project, @user) } - before do - allow(refresh_service).to receive(:execute_hooks) + + it 'refreshes the merge request' do + expect(refresh_service).to receive(:execute_hooks). + with(@fork_merge_request, 'update') + allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) + refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') reload_mrs - end - it 'should execute hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update') - end + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('Source branch `master` restored') } - it { expect(@fork_merge_request).to be_open } + notes = @fork_merge_request.notes.reorder(:created_at).map(&:note) + expect(notes[0]).to include('Source branch `master` restored') + expect(notes[1]).to include('Added 4 commits') + expect(@fork_merge_request).to be_open + end end def reload_mrs diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 16b1c66ff9..699b2e3441 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -229,7 +229,7 @@ describe SystemNoteService do end describe '.change_branch' do - subject { described_class.change_branch(noteable, project, author, :target, old_branch, new_branch) } + subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } let(:old_branch) { 'old_branch'} let(:new_branch) { 'new_branch'} @@ -243,7 +243,7 @@ describe SystemNoteService do end describe '.change_branch_presence' do - subject { described_class.change_branch_presence(noteable, project, author, 'source', 'feature', :delete) } + subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) } it_behaves_like 'a system note' From 9f9f0c35ecd9f7a5a057030253791d051f832f6d Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 12 Oct 2015 12:04:20 +0200 Subject: [PATCH 137/443] Show merge requests which close current issue --- CHANGELOG | 1 + app/assets/stylesheets/pages/issues.scss | 5 +++++ app/controllers/projects/issues_controller.rb | 7 +++++++ app/controllers/projects/merge_requests_controller.rb | 2 +- app/helpers/issues_helper.rb | 4 ++++ app/models/issue.rb | 10 ++++++++++ app/models/merge_request.rb | 4 ++++ app/views/projects/issues/_closed_by_box.html.haml | 6 ++++++ app/views/projects/issues/show.html.haml | 3 ++- 9 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 app/views/projects/issues/_closed_by_box.html.haml diff --git a/CHANGELOG b/CHANGELOG index 3992669214..e15fc9cb24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x + - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) - Make diff file view easier to use on mobile screens (Stan Hu) - Improved performance of finding users by username or Email address - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu) diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 4bf58cb4a5..41c069f0ad 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -132,6 +132,11 @@ form.edit-issue { } } +.issue-closed-by-widget { + padding: 16px 0; + margin: 0px; +} + .issue-form .select2-container { width: 250px !important; } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 97485c101f..eaf1400924 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -14,6 +14,9 @@ class Projects::IssuesController < Projects::ApplicationController # Allow issues bulk update before_action :authorize_admin_issues!, only: [:bulk_update] + # Cross-reference merge requests + before_action :closed_by_merge_requests, only: [:show] + respond_to :html def index @@ -112,6 +115,10 @@ class Projects::IssuesController < Projects::ApplicationController render nothing: true end + def closed_by_merge_requests + @closed_by_mr = @issue.closed_by_merge_requests(current_user) + end + protected def issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 98df6984bf..0d9c546195 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -259,7 +259,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.commits @merge_request_diff = @merge_request.merge_request_diff - + if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 6ddb37cd0d..1adbf3a79b 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -83,6 +83,10 @@ module IssuesHelper end end + def merge_requests_sentence(merge_requests) + merge_requests.map(&:to_reference).to_sentence + end + # Required for Gitlab::Markdown::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/models/issue.rb b/app/models/issue.rb index fc7e9abe29..c24a329847 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -95,4 +95,14 @@ class Issue < ActiveRecord::Base def source_project project end + + # From all notes on this issue, we'll select the system notes about linked + # merge requests. Of those, the MRs closing `self` are returned. + def closed_by_merge_requests(current_user) + notes.system.flat_map do |note| + ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + ext.analyze(note.note) + ext.merge_requests + end.uniq.select { |mr| mr.closes_issue?(self) } + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c83b15c7d3..3ae74ceac6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -294,6 +294,10 @@ class MergeRequest < ActiveRecord::Base target_project end + def closes_issue?(issue) + closes_issues.include?(issue) + end + # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml new file mode 100644 index 0000000000..fe886b6d7d --- /dev/null +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -0,0 +1,6 @@ +.issue-closed-by-widget + %i.fa.fa-check + - if @closed_by_mr.count == 1 + This issue will be closed when #{gfm(@closed_by_mr.first.to_reference)} is accepted. + -else + This issue will be closed when any of merge requests #{gfm(merge_requests_sentence(@closed_by_mr.sort))} is accepted. diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5cb814c9ea..309f276882 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -46,6 +46,7 @@ = markdown(@issue.description) %textarea.hidden.js-task-list-field = @issue.description - + - if @closed_by_mr.present? + = render 'projects/issues/closed_by_box' .issue-discussion = render 'projects/issues/discussion' From f726df26c28666e640e566d50f363994e71cf681 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Oct 2015 00:51:25 -0700 Subject: [PATCH 138/443] Reorder system note verb to say "Restored source branch X" instead of "Source branch X restored" --- app/services/system_note_service.rb | 4 ++-- spec/services/merge_requests/refresh_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index c0a5c3aeb5..37f454cfc3 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -179,7 +179,7 @@ class SystemNoteService # # Example Note text: # - # "Target branch `feature` deleted" + # "Restored target branch `feature`" # # Returns the created Note object def self.change_branch_presence(noteable, project, author, branch_type, branch, presence) @@ -189,7 +189,7 @@ class SystemNoteService else 'deleted' end - body = "#{branch_type.to_s} branch `#{branch}` #{verb}".capitalize + body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 463cd594fb..227ac995ec 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -121,7 +121,7 @@ describe MergeRequests::RefreshService do expect(@merge_request).to be_open notes = @fork_merge_request.notes.reorder(:created_at).map(&:note) - expect(notes[0]).to include('Source branch `master` restored') + expect(notes[0]).to include('Restored source branch `master`') expect(notes[1]).to include('Added 4 commits') expect(@fork_merge_request).to be_open end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 699b2e3441..a45130bd47 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -249,7 +249,7 @@ describe SystemNoteService do context 'when source branch deleted' do it 'sets the note text' do - expect(subject.note).to eq "Source branch `feature` deleted" + expect(subject.note).to eq "Deleted source branch `feature`" end end end From 94a788f66dfcc13ad02855b05c38826f958038af Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 13 Oct 2015 09:41:46 +0200 Subject: [PATCH 139/443] Only accept open issues and merge requests --- app/controllers/projects/issues_controller.rb | 2 +- app/helpers/issues_helper.rb | 2 +- app/models/concerns/issuable.rb | 4 ++ app/models/issue.rb | 10 ++--- app/models/merge_request.rb | 4 -- .../projects/issues/_closed_by_box.html.haml | 9 ++--- app/views/projects/issues/show.html.haml | 2 +- spec/helpers/issues_helper_spec.rb | 10 +++++ spec/models/concerns/issuable_spec.rb | 1 - spec/models/issue_spec.rb | 37 +++++++++++++++++++ 10 files changed, 62 insertions(+), 19 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index eaf1400924..cc8321d97a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -116,7 +116,7 @@ class Projects::IssuesController < Projects::ApplicationController end def closed_by_merge_requests - @closed_by_mr = @issue.closed_by_merge_requests(current_user) + @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) end protected diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 1adbf3a79b..fda18e7b31 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -84,7 +84,7 @@ module IssuesHelper end def merge_requests_sentence(merge_requests) - merge_requests.map(&:to_reference).to_sentence + merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') end # Required for Gitlab::Markdown::IssueReferenceFilter diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0e8bcc1a4e..efa6a26999 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -86,6 +86,10 @@ module Issuable assignee_id_changed? end + def open? + opened? || reopened? + end + # # Votes # diff --git a/app/models/issue.rb b/app/models/issue.rb index c24a329847..7218310803 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -98,11 +98,11 @@ class Issue < ActiveRecord::Base # From all notes on this issue, we'll select the system notes about linked # merge requests. Of those, the MRs closing `self` are returned. - def closed_by_merge_requests(current_user) + def closed_by_merge_requests(current_user = nil) + return [] unless open? + notes.system.flat_map do |note| - ext = Gitlab::ReferenceExtractor.new(self.project, current_user) - ext.analyze(note.note) - ext.merge_requests - end.uniq.select { |mr| mr.closes_issue?(self) } + note.all_references(current_user).merge_requests + end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 3ae74ceac6..0042b95c4f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -222,10 +222,6 @@ class MergeRequest < ActiveRecord::Base self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end - def open? - opened? || reopened? - end - def work_in_progress? !!(title =~ /\A\[?WIP\]?:? /i) end diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index fe886b6d7d..aef352029d 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,6 +1,3 @@ -.issue-closed-by-widget - %i.fa.fa-check - - if @closed_by_mr.count == 1 - This issue will be closed when #{gfm(@closed_by_mr.first.to_reference)} is accepted. - -else - This issue will be closed when any of merge requests #{gfm(merge_requests_sentence(@closed_by_mr.sort))} is accepted. +.issue-closed-by-widget + = icon('check') + This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted. diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 309f276882..f01bf2505d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -46,7 +46,7 @@ = markdown(@issue.description) %textarea.hidden.js-task-list-field = @issue.description - - if @closed_by_mr.present? + - if @closed_by_merge_requests.present? = render 'projects/issues/closed_by_box' .issue-discussion = render 'projects/issues/discussion' diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index c08ddb4cae..78a6b631eb 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -117,4 +117,14 @@ describe IssuesHelper do end end + describe "#merge_requests_sentence" do + subject { merge_requests_sentence(merge_requests)} + let(:merge_requests) do + [ build(:merge_request, iid: 1), build(:merge_request, iid: 2), + build(:merge_request, iid: 3)] + end + + it { is_expected.to eq("!1, !2, or !3") } + end + end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 8f706f8934..0f13c4410c 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -68,7 +68,6 @@ describe Issue, "Issuable" do end end - describe "#to_hook_data" do let(:hook_data) { issue.to_hook_data(user) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 623332cd2f..c9aa1b063c 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -68,6 +68,43 @@ describe Issue do end end + describe '#closed_by_merge_requests' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project, state: "opened")} + let(:closed_issue) { build(:issue, project: project, state: "closed")} + + let(:mr) do + opts = { + title: 'Awesome merge_request', + description: "Fixes #{issue.to_reference}", + source_branch: 'feature', + target_branch: 'master' + } + MergeRequests::CreateService.new(project, project.owner, opts).execute + end + + let(:closed_mr) do + opts = { + title: 'Awesome merge_request 2', + description: "Fixes #{issue.to_reference}", + source_branch: 'feature', + target_branch: 'master', + state: 'closed' + } + MergeRequests::CreateService.new(project, project.owner, opts).execute + end + + it 'returns the merge request to close this issue' do + allow(mr).to receive(:closes_issue?).with(issue).and_return(true) + + expect(issue.closed_by_merge_requests).to eq([mr]) + end + + it "returns an empty array when the current issue is closed already" do + expect(closed_issue.closed_by_merge_requests).to eq([]) + end + end + it_behaves_like 'an editable mentionable' do subject { create(:issue) } From d1450af81f472ff0221454fe048e96cdd8dd11e7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 10:10:33 +0200 Subject: [PATCH 140/443] Add tests for last commit info on project home page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 2 ++ features/project/project.feature | 6 ++++++ features/steps/shared/project.rb | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b6d53acd11..9fa853a6a3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -513,6 +513,8 @@ pre.light-well { } .project-last-commit { + margin: 0 7px; + .ci-status { margin-right: 16px; } diff --git a/features/project/project.feature b/features/project/project.feature index b3fb079454..1a53945eb0 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -31,6 +31,12 @@ Feature: Project And I visit project "Shop" page Then I should see project "Shop" README + Scenario: I should see last commit with CI + Given project "Shop" has CI enabled + Given project "Shop" has CI build + And I visit project "Shop" page + And I should see last commit with CI status + @javascript Scenario: I should see project activity When I visit project "Shop" activity page diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 5744e455eb..7021fac5fe 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -206,4 +206,11 @@ module SharedProject project = Project.find_by(name: "Shop") create :ci_commit, gl_project: project, sha: project.commit.sha end + + step 'I should see last commit with CI status' do + page.within ".project-last-commit" do + expect(page).to have_content(project.commit.sha[0..6]) + expect(page).to have_content("skipped") + end + end end From 66584f72fd7e9fa8657a711aa12864b76fb71ce4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 11:15:12 +0200 Subject: [PATCH 141/443] Add changelog item Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index aba823948a..cca8b38ef7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.2.0 (unreleased) + - Show last project commit to default branch on project home page + v 8.1.0 (unreleased) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x From 64504b636470ad1048ba6310d6bd2dff8a28b914 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 13 Oct 2015 16:04:36 +0200 Subject: [PATCH 142/443] Add an index to milestones title and label title --- db/migrate/20151013133938_add_index_to_milestones.rb | 6 ++++++ db/schema.rb | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 db/migrate/20151013133938_add_index_to_milestones.rb diff --git a/db/migrate/20151013133938_add_index_to_milestones.rb b/db/migrate/20151013133938_add_index_to_milestones.rb new file mode 100644 index 0000000000..41cd91e570 --- /dev/null +++ b/db/migrate/20151013133938_add_index_to_milestones.rb @@ -0,0 +1,6 @@ +class AddIndexToMilestones < ActiveRecord::Migration + def change + add_index :milestones, :title + add_index :labels, :title + end +end diff --git a/db/schema.rb b/db/schema.rb index 7a11dfca03..68bd9d2c3e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -410,6 +410,7 @@ ActiveRecord::Schema.define(version: 20151008130321) do end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree + add_index "labels", ["title"], name: "index_labels_on_title", using: :btree create_table "members", force: true do |t| t.integer "access_level", null: false @@ -491,6 +492,7 @@ ActiveRecord::Schema.define(version: 20151008130321) do add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree + add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree create_table "namespaces", force: true do |t| t.string "name", null: false From b5762104abbf373d69a20532de08564eb9ae93f6 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 13 Oct 2015 16:05:04 +0200 Subject: [PATCH 143/443] Minor refactoring in seeding --- db/fixtures/development/05_users.rb | 4 ++-- db/fixtures/development/07_milestones.rb | 2 +- db/fixtures/development/09_issues.rb | 2 +- db/fixtures/development/12_snippets.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index 378354efd5..03da29c4c6 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -1,5 +1,5 @@ Gitlab::Seeder.quiet do - (2..20).each do |i| + 20.times do |i| begin User.create!( username: FFaker::Internet.user_name, @@ -15,7 +15,7 @@ Gitlab::Seeder.quiet do end end - (1..5).each do |i| + 5.times do |i| begin User.create!( username: "user#{i}", diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index a43116829d..e028ac82ba 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -1,6 +1,6 @@ Gitlab::Seeder.quiet do Project.all.each do |project| - (1..5).each do |i| + 5.times do |i| milestone_params = { title: "v#{i}.0", description: FFaker::Lorem.sentence, diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index c636e96381..4fa572fca9 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -1,6 +1,6 @@ Gitlab::Seeder.quiet do Project.all.each do |project| - (1..10).each do |i| + 10.times do issue_params = { title: FFaker::Lorem.sentence(6), description: FFaker::Lorem.sentence, diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index 3bd4b442ad..74898544a6 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -22,7 +22,7 @@ class Member < ActiveRecord::Base end eos - (1..50).each do |i| + 50.times do |i| user = User.all.sample PersonalSnippet.seed(:id, [{ From ac44e3844deac1c13f35ca0e9a7ce995be58aab7 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 14 Oct 2015 12:20:48 +0200 Subject: [PATCH 144/443] Add project scope to milestone search --- app/finders/issuable_finder.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 97c7e74c29..d60f36e1af 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -81,7 +81,14 @@ class IssuableFinder @milestones = if milestones? - Milestone.where(title: params[:milestone_title]) + scope = + if project + project.milestones + else + Milestone.all + end + + scope.where(title: params[:milestone_title]) else nil end From 9127ae5ca80aa06b0a83d275e2a2d9b7ccfbfc3d Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 14 Oct 2015 12:23:49 +0200 Subject: [PATCH 145/443] Improve performance of queries Credits to Douwe Maan --- CHANGELOG | 1 + app/finders/issuable_finder.rb | 74 ++++++++++++------- .../20151013133938_add_index_to_milestones.rb | 6 -- 3 files changed, 47 insertions(+), 34 deletions(-) delete mode 100644 db/migrate/20151013133938_add_index_to_milestones.rb diff --git a/CHANGELOG b/CHANGELOG index 814a6772cf..8f696009d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -63,6 +63,7 @@ v 8.1.0 (unreleased) - Only render 404 page from /public - Hide passwords from services API (Alex Lossent) - Fix: Images cannot show when projects' path was changed + - Optimize query when filtering on issuables (Zeger-Jan van de Weg) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index d60f36e1af..3170c0f672 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -53,15 +53,36 @@ class IssuableFinder end end + def project? + params[:project_id].present? + end + def project return @project if defined?(@project) - @project = - if params[:project_id].present? - Project.find(params[:project_id]) - else - nil - end + if project? + @project = Project.find(params[:project_id]) + + unless Ability.abilities.allowed?(current_user, :read_project, @project) + @project = nil + end + else + @project = nil + end + + @project + end + + def projects + return if project? + + return @projects if defined?(@projects) + + if current_user && params[:authorized_only].presence && !current_user_related? + current_user.authorized_projects + else + ProjectsFinder.new.execute(current_user) + end end def search @@ -84,8 +105,10 @@ class IssuableFinder scope = if project project.milestones + elsif projects + Milestone.where(project_id: projects) else - Milestone.all + Milestone.none end scope.where(title: params[:milestone_title]) @@ -127,19 +150,7 @@ class IssuableFinder private def init_collection - table_name = klass.table_name - - if project - if Ability.abilities.allowed?(current_user, :read_project, project) - project.send(table_name) - else - [] - end - elsif current_user && params[:authorized_only].presence && !current_user_related? - klass.of_projects(current_user.authorized_projects).references(:project) - else - klass.of_projects(ProjectsFinder.new.execute(current_user)).references(:project) - end + klass.all end def by_scope(items) @@ -177,7 +188,14 @@ class IssuableFinder end def by_project(items) - items = items.of_projects(project.id) if project + items = + if project + items.of_projects(project) + elsif projects + items.of_projects(projects).references(:project) + else + items.none + end items end @@ -223,17 +241,17 @@ class IssuableFinder def by_label(items) if params[:label_name].present? if params[:label_name] == Label::None.title - item_ids = LabelLink.where(target_type: klass.name).pluck(:target_id) - - items = items.where('id NOT IN (?)', item_ids) + items = items. + joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). + where(label_links: { id: nil }) else label_names = params[:label_name].split(",") - item_ids = LabelLink.joins(:label). - where('labels.title in (?)', label_names). - where(target_type: klass.name).pluck(:target_id) + items = items.joins(:labels).where(labels: { title: label_names }) - items = items.where(id: item_ids) + if project + items = items.where('labels.project_id = :id', id: project.id) + end end end diff --git a/db/migrate/20151013133938_add_index_to_milestones.rb b/db/migrate/20151013133938_add_index_to_milestones.rb deleted file mode 100644 index 41cd91e570..0000000000 --- a/db/migrate/20151013133938_add_index_to_milestones.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AddIndexToMilestones < ActiveRecord::Migration - def change - add_index :milestones, :title - add_index :labels, :title - end -end From 0108cdf49514dcaccc6a53c7b6e257fa9acfea98 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 16 Oct 2015 11:43:26 +0200 Subject: [PATCH 146/443] Improve performance of filtering issues by milestone --- app/finders/issuable_finder.rb | 63 ++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3170c0f672..f00bb02d0f 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -74,11 +74,11 @@ class IssuableFinder end def projects - return if project? - return @projects if defined?(@projects) - if current_user && params[:authorized_only].presence && !current_user_related? + if project? + project + elsif current_user && params[:authorized_only].presence && !current_user_related? current_user.authorized_projects else ProjectsFinder.new.execute(current_user) @@ -102,14 +102,7 @@ class IssuableFinder @milestones = if milestones? - scope = - if project - project.milestones - elsif projects - Milestone.where(project_id: projects) - else - Milestone.none - end + scope = Milestone.where(project_id: projects) scope.where(title: params[:milestone_title]) else @@ -117,6 +110,14 @@ class IssuableFinder end end + def labels? + params[:label_name].present? + end + + def no_labels? + labels? && params[:label_name] == Label::None.title + end + def assignee? params[:assignee_id].present? end @@ -189,9 +190,7 @@ class IssuableFinder def by_project(items) items = - if project - items.of_projects(project) - elsif projects + if projects items.of_projects(projects).references(:project) else items.none @@ -210,18 +209,6 @@ class IssuableFinder items.sort(params[:sort]) end - def by_milestone(items) - if milestones? - if no_milestones? - items = items.where(milestone_id: [-1, nil]) - else - items = items.where(milestone_id: milestones.try(:pluck, :id)) - end - end - - items - end - def by_assignee(items) if assignee? items = items.where(assignee_id: assignee.try(:id)) @@ -238,9 +225,25 @@ class IssuableFinder items end + def by_milestone(items) + if milestones? + if no_milestones? + items = items.where(milestone_id: [-1, nil]) + else + items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) + + if projects + items = items.where(milestones: { project_id: projects }) + end + end + end + + items + end + def by_label(items) - if params[:label_name].present? - if params[:label_name] == Label::None.title + if labels? + if no_labels? items = items. joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). where(label_links: { id: nil }) @@ -249,8 +252,8 @@ class IssuableFinder items = items.joins(:labels).where(labels: { title: label_names }) - if project - items = items.where('labels.project_id = :id', id: project.id) + if projects + items = items.where(labels: { project_id: projects }) end end end From 7c85ebf6dc9c083d53642cbe2b6a5276c6b95aa6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 13:24:28 +0200 Subject: [PATCH 147/443] Partly implement new UI for user page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/blocks.scss | 45 +++++++++ app/assets/stylesheets/pages/profile.scss | 6 ++ .../{ => admin}/users/_profile.html.haml | 0 app/views/admin/users/show.html.haml | 2 +- app/views/users/calendar.html.haml | 6 +- app/views/users/show.html.haml | 94 ++++++++++++------- features/steps/abuse_reports.rb | 2 +- 7 files changed, 114 insertions(+), 41 deletions(-) rename app/views/{ => admin}/users/_profile.html.haml (100%) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 6ce34b5c3e..a09339050c 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -60,3 +60,48 @@ line-height: 42px; } } + +.cover-block { + text-align: center; + background: #f7f8fa; + margin: -$gl-padding; + margin-bottom: 0; + padding: 44px $gl-padding; + border-bottom: 1px solid $border-color; + position: relative; + + .avatar-holder { + margin-bottom: 16px; + + .avatar, .identicon { + margin: 0 auto; + float: none; + } + + .identicon { + @include border-radius(50%); + } + } + + .cover-title { + color: $gl-header-color; + margin: 0; + font-size: 23px; + font-weight: normal; + margin: 16px 0 5px 0; + color: #4c4e54; + font-size: 23px; + line-height: 1.1; + } + + .cover-desc { + padding: 0 $gl-padding; + color: $gl-text-color; + } + + .cover-controls { + position: absolute; + top: 10px; + right: 10px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8e4f0eb2b2..b7391e5303 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -47,3 +47,9 @@ } } } + +.calendar-hint { + margin-top: -12px; + float: right; + font-size: 12px; +} diff --git a/app/views/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml similarity index 100% rename from app/views/users/_profile.html.haml rename to app/views/admin/users/_profile.html.haml diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 231bcb0426..0848504b7a 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -14,7 +14,7 @@ %strong = link_to user_path(@user) do = @user.username - = render 'users/profile', user: @user + = render 'admin/users/profile', user: @user .panel.panel-default .panel-heading diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 922b0c6ceb..7f29918dba 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,7 +1,3 @@ -%h4 - Contributions calendar - .pull-right - %small Issues, merge requests and push events #cal-heatmap.calendar :javascript new Calendar( @@ -10,3 +6,5 @@ #{@starting_month}, '#{user_calendar_activities_path}' ); + +.calendar-hint Summary of issues, merge requests and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 2a64708d07..4ea4a1f92c 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -6,47 +6,72 @@ = render 'shared/show_aside' -.row +.cover-block + .avatar-holder + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' + .cover-title + = @user.name + + .cover-desc + %span + @#{@user.username}. + - if @user.bio.present? + %span + #{@user.bio}. + %span + Member since #{@user.created_at.stamp("Aug 21, 2011")} + + .cover-desc + - unless @user.public_email.blank? + = link_to @user.public_email, "mailto:#{@user.public_email}" + - unless @user.skype.blank? + · + = link_to "Skype", "skype:#{@user.skype}" + - unless @user.linkedin.blank? + · + = link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}" + - unless @user.twitter.blank? + · + = link_to "Twitter", "http://www.twitter.com/#{@user.twitter}" + - unless @user.website_url.blank? + · + = link_to @user.short_website_url, @user.full_website_url + - unless @user.location.blank? + · + = @user.location + + + .cover-controls + - if @user == current_user + = link_to profile_path, class: 'btn btn-gray' do + = icon('pencil') + - elsif current_user + .report-abuse + - if @user.abuse_report + %button.btn.btn-danger{ title: 'Already reported for abuse', + data: { toggle: 'tooltip', placement: 'left', container: 'body' }} + = icon('exclamation-circle') + - else + = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', + title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do + = icon('exclamation-circle') + +.gray-content-block.second-block + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities + + +.row.prepend-top-20 %section.col-md-7 - .header-with-avatar - = link_to avatar_icon(@user, 400), target: '_blank' do - = image_tag avatar_icon(@user, 90), class: "avatar avatar-tile s90", alt: '' - %h3 - = @user.name - - if @user == current_user - .pull-right.hidden-xs - = link_to profile_path, class: 'btn btn-sm' do - = icon('user') - Profile settings - - elsif current_user - .report_abuse.pull-right - - if @user.abuse_report - %span#report_abuse_btn.light.btn.btn-sm.btn-close{title: 'Already reported for abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}} - = icon('exclamation-circle') - - else - %a.light.btn.btn-sm{href: new_abuse_report_path(user_id: @user.id), title: 'Report abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}} - = icon('exclamation-circle') - - .username - @#{@user.username} - .description - - if @user.bio.present? - = @user.bio - - .clearfix - - if @groups.any? .prepend-top-20 %h4 Groups = render 'groups', groups: @groups %hr - .hidden-xs - .user-calendar - %h4.center.light - %i.fa.fa-spinner.fa-spin - .user-calendar-activities - %hr %h4 User Activity @@ -59,7 +84,6 @@ .content_list = spinner %aside.col-md-5 - = render 'profile', user: @user = render 'projects', projects: @projects, contributed_projects: @contributed_projects :coffeescript diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb index 56652ff6f0..499accb0b0 100644 --- a/features/steps/abuse_reports.rb +++ b/features/steps/abuse_reports.rb @@ -23,7 +23,7 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps end step 'I should see a red "Report abuse" button' do - expect(find(:css, '.report_abuse')).to have_selector(:css, 'span.btn-close') + expect(page).to have_button("Already reported for abuse") end def user_mike From 47e17103f589b51aa2f6267f56a67c1eb400054f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 13:36:48 +0200 Subject: [PATCH 148/443] Change order of sha and commit message on project home page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 2 +- app/views/projects/_last_commit.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9fa853a6a3..0dddb6b6ed 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -524,7 +524,7 @@ pre.light-well { } .commit_short_id { - margin-left: 5px; + margin-right: 5px; color: $gl-link-color; font-weight: 600; } diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 0c16c3ccbf..d7b20bfc6b 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -5,8 +5,8 @@ = ci_status_icon(ci_commit) = ci_commit.status - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" · #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by = commit_author_link(commit, avatar: true, size: 24) From c6be5006daa8a5a198bc06f8b8f07109535189ca Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 16 Oct 2015 15:29:50 +0200 Subject: [PATCH 149/443] Add index on ci_commits.gl_project_id Fixes #3086 --- .../20151016131433_add_ci_projects_gl_project_id_index.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb diff --git a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb new file mode 100644 index 0000000000..52a47aa9c5 --- /dev/null +++ b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb @@ -0,0 +1,5 @@ +class AddCiProjectsGlProjectIdIndex < ActiveRecord::Migration + def change + add_index :ci_commits, :gl_project_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 7a11dfca03..885756fef1 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: 20151008130321) do +ActiveRecord::Schema.define(version: 20151016131433) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -130,6 +130,7 @@ ActiveRecord::Schema.define(version: 20151008130321) do t.integer "gl_project_id" end + add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree From a7fafee52124df6ffa9e6e1ecee6ff6884c6ce95 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 16 Oct 2015 08:45:50 -0500 Subject: [PATCH 150/443] Fix import from SVN link --- app/views/projects/imports/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index f8f2e192e2..92a87690c5 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -17,6 +17,6 @@ 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"} + For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"} .form-actions = f.submit 'Start import', class: "btn btn-create", tabindex: 4 From 46d748dc2e7329fede6fd2c20038e98100b90fec Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 16:06:11 +0200 Subject: [PATCH 151/443] Highlight comment based on anchor in URL Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/assets/stylesheets/framework/timeline.scss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ea9f55ad08..57768d6b3e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Show last project commit to default branch on project home page + - Highlight comment based on anchor in URL v 8.1.0 (unreleased) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index bf21d7fce7..9d6f053aef 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -13,6 +13,10 @@ border-bottom: 1px solid #ECEEF1; border-right: 1px solid #ECEEF1; + &:target { + background: $hover; + } + &:last-child { border-bottom: none; } From f5b9c3d59d0a9fff4641e6028357213998137897 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 16:15:30 +0200 Subject: [PATCH 152/443] Make selectbox options more compact Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/selects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index cba621635b..5bdd81777a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -32,7 +32,7 @@ } .select2-results .select2-result-label { - padding: 16px; + padding: 6px; } .select2-drop{ From db1e7fb8ddb019c12fee0e6c51426dcad0ce099b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 16:25:57 +0200 Subject: [PATCH 153/443] Dont put padding on typography but on holder elements instead Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/typography.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6ccc084526..1f4341b246 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -207,9 +207,7 @@ a > code { */ .wiki { @include md-typography; - word-wrap: break-word; - padding: 7px; /* Link to current header. */ h1, h2, h3, h4, h5, h6 { From b4cc05e56e6178b55d554ab95da051fe91a4765b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 16:36:37 +0200 Subject: [PATCH 154/443] Add extra padding to some markdown pages Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/help.scss | 4 ++++ app/assets/stylesheets/pages/projects.scss | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 6da7a2511a..bd224705f0 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -68,3 +68,7 @@ body.modal-open { .modal .modal-dialog { width: 860px; } + +.documentation { + padding: 7px; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0dddb6b6ed..48b8775026 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -542,3 +542,7 @@ pre.light-well { } } } + +.project-show-readme .readme-holder { + padding: 7px; +} From 6909b309935f32da6bcccb1148242f19eec5de26 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 16 Oct 2015 16:39:31 +0200 Subject: [PATCH 155/443] Fix missing commit status for widget when no CI service is enabled --- .../merge_requests/widget/_heading.html.haml | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 68dda1424c..10efb81193 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,44 +1,43 @@ -- if @merge_request.has_ci? - - ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha) - - if ci_commit - - status = ci_commit.status - .mr-widget-heading - .ci_widget{class: "ci-#{status}"} - = ci_status_icon(ci_commit) +- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha) +- if ci_commit + - status = ci_commit.status + .mr-widget-heading + .ci_widget{class: "ci-#{status}"} + = ci_status_icon(ci_commit) + %span CI build #{status} + for #{@merge_request.last_commit_short_sha}. + %span.ci-coverage + = link_to "View build details", ci_status_path(ci_commit) + +- elsif @merge_request.has_ci? + - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + - # Remove in later versions when services like Jenkins will set CI status via Commit status API + .mr-widget-heading + - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| + .ci_widget{class: "ci-#{status}", style: "display:none"} + - if status == :success + - status = "passed" + = icon("check-circle") + - else + = icon("circle") %span CI build #{status} for #{@merge_request.last_commit_short_sha}. %span.ci-coverage - = link_to "View build details", ci_status_path(ci_commit) + - if ci_build_details_path(@merge_request) + = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - - else - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API - .mr-widget-heading - - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} - - if status == :success - - status = "passed" - = icon("check-circle") - - else - = icon("circle") - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. - %span.ci-coverage - - if ci_build_details_path(@merge_request) - = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + .ci_widget + = icon("spinner spin") + Checking CI status for #{@merge_request.last_commit_short_sha}… - .ci_widget - = icon("spinner spin") - Checking CI status for #{@merge_request.last_commit_short_sha}… + .ci_widget.ci-not_found{style: "display:none"} + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. - .ci_widget.ci-not_found{style: "display:none"} - = icon("times-circle") - Could not find CI status for #{@merge_request.last_commit_short_sha}. + .ci_widget.ci-error{style: "display:none"} + = icon("times-circle") + Could not connect to the CI server. Please check your settings and try again. - .ci_widget.ci-error{style: "display:none"} - = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again. - - :coffeescript - $ -> - merge_request_widget.getCiStatus() + :coffeescript + $ -> + merge_request_widget.getCiStatus() From 1543989d63981c65424ae053e6b8aa85ad850113 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 17:06:24 +0200 Subject: [PATCH 156/443] Improve markdown typography scss Signed-off-by: Dmitriy Zaporozhets --- .../stylesheets/framework/typography.scss | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 1f4341b246..1857c1659a 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,5 +1,6 @@ @mixin md-typography { color: $md-text-color; + word-wrap: break-word; a { color: $md-link-color; @@ -73,6 +74,8 @@ } blockquote { + color: #7f8fa4; + font-size: inherit; padding: 8px 21px; margin: 12px 0 12px; border-left: 3px solid #e7e9ed; @@ -80,7 +83,7 @@ blockquote p { color: #7f8fa4 !important; - font-size: 15px; + font-size: inherit; line-height: 1.5; } @@ -112,9 +115,9 @@ font-weight: inherit; } - - ul { - color: #5c5d5e; + ul, ol { + padding: 0; + margin: 6px 0 6px 18px !important; } li { @@ -136,6 +139,33 @@ text-decoration: none; } } + + /* Link to current header. */ + h1, h2, h3, h4, h5, h6 { + position: relative; + + a.anchor { + // Setting `display: none` would prevent the anchor being scrolled to, so + // instead we set the height to 0 and it gets updated on hover. + height: 0; + } + + &:hover > a.anchor { + $size: 16px; + position: absolute; + right: 100%; + top: 50%; + margin-top: -$size/2; + margin-right: 0px; + padding-right: 20px; + display: inline-block; + width: $size; + height: $size; + background-image: image-url("icon-link.png"); + background-size: contain; + background-repeat: no-repeat; + } + } } @@ -202,47 +232,11 @@ a > code { } /** - * Wiki typography + * Apply Markdown typography * */ .wiki { @include md-typography; - word-wrap: break-word; - - /* Link to current header. */ - h1, h2, h3, h4, h5, h6 { - position: relative; - - a.anchor { - // Setting `display: none` would prevent the anchor being scrolled to, so - // instead we set the height to 0 and it gets updated on hover. - height: 0; - } - - &:hover > a.anchor { - $size: 16px; - position: absolute; - right: 100%; - top: 50%; - margin-top: -$size/2; - margin-right: 0px; - padding-right: 20px; - display: inline-block; - width: $size; - height: $size; - background-image: image-url("icon-link.png"); - background-size: contain; - background-repeat: no-repeat; - } - } - - ul,ol { - padding: 0; - margin: 6px 0 6px 18px !important; - } - ol { - color: #5c5d5e; - } } .md-area { From 58260a0327a953499a07e9cad8d9aaad2d25699b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 16 Oct 2015 17:16:17 +0200 Subject: [PATCH 157/443] Do no rely on basename of builds, uploads --- lib/backup/builds.rb | 6 +++++- lib/backup/files.rb | 9 +++++---- lib/backup/uploads.rb | 6 +++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb index d269f8e260..800f30c214 100644 --- a/lib/backup/builds.rb +++ b/lib/backup/builds.rb @@ -1,7 +1,11 @@ module Backup class Builds < Files def initialize - super(Settings.gitlab_ci.builds_path) + super('builds', Settings.gitlab_ci.builds_path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) end end end diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 5a210a0e46..654b4d1c89 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -4,9 +4,9 @@ module Backup class Files attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir - def initialize(app_files_dir) + def initialize(name, app_files_dir) + @name = name @app_files_dir = File.realpath(app_files_dir) - @name = File.basename(app_files_dir) @files_parent_dir = File.realpath(File.join(@app_files_dir, '..')) @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz') end @@ -15,13 +15,14 @@ module Backup def dump FileUtils.mkdir_p(Gitlab.config.backup.path) FileUtils.rm_f(backup_tarball) - run_pipeline!([%W(tar -C #{files_parent_dir} -cf - #{name}), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600]) end def restore backup_existing_files_dir + create_files_dir - run_pipeline!([%W(gzip -cd), %W(tar -C #{files_parent_dir} -xf -)], in: backup_tarball) + run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball) end def backup_existing_files_dir diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 7c0838cc8b..0a0ec564ba 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -2,7 +2,11 @@ module Backup class Uploads < Files def initialize - super(Rails.root.join('public/uploads')) + super('uploads', Rails.root.join('public/uploads')) + end + + def create_files_dir + Dir.mkdir(app_files_dir) end end end From f6664601719925834fbf6784545ee64ad9413a12 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 15:18:21 +0000 Subject: [PATCH 158/443] Increase dropdown padding a bit --- app/assets/stylesheets/framework/selects.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 5bdd81777a..78fff58d23 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -32,7 +32,7 @@ } .select2-results .select2-result-label { - padding: 6px; + padding: 9px; } .select2-drop{ @@ -143,4 +143,4 @@ .ajax-users-dropdown { min-width: 250px !important; -} +} \ No newline at end of file From f405954764b8bea32e54b3cf59e06a5469781bf3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 16 Oct 2015 21:58:30 +0200 Subject: [PATCH 159/443] Added indexes for notes.line_code and CI columns This adds indexes for the following columns: * notes.line_code * ci_projects.gitlab_id * ci_projects.shared_runners_enabled * ci_builds.type * ci_builds.status --- .../20151016195451_add_ci_builds_and_projects_indexes.rb | 9 +++++++++ db/migrate/20151016195706_add_notes_line_code_index.rb | 5 +++++ db/schema.rb | 8 +++++++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb create mode 100644 db/migrate/20151016195706_add_notes_line_code_index.rb diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb new file mode 100644 index 0000000000..7f1af1c758 --- /dev/null +++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb @@ -0,0 +1,9 @@ +class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration + def change + add_index :ci_projects, :gitlab_id + add_index :ci_projects, :shared_runners_enabled + + add_index :ci_builds, :type + add_index :ci_builds, :status + end +end diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb new file mode 100644 index 0000000000..aeeb1a759f --- /dev/null +++ b/db/migrate/20151016195706_add_notes_line_code_index.rb @@ -0,0 +1,5 @@ +class AddNotesLineCodeIndex < ActiveRecord::Migration + def change + add_index :notes, :line_code + end +end diff --git a/db/schema.rb b/db/schema.rb index 885756fef1..886b05f3e5 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: 20151016131433) do +ActiveRecord::Schema.define(version: 20151016195706) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -115,6 +115,8 @@ ActiveRecord::Schema.define(version: 20151016131433) do add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree + add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree + add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree create_table "ci_commits", force: true do |t| t.integer "project_id" @@ -190,6 +192,9 @@ ActiveRecord::Schema.define(version: 20151016131433) do t.text "generated_yaml_config" end + add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree + add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree + create_table "ci_runner_projects", force: true do |t| t.integer "runner_id", null: false t.integer "project_id", null: false @@ -530,6 +535,7 @@ ActiveRecord::Schema.define(version: 20151016131433) do add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree + add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree From efd5472c4b957fd30e745914337f5c16be6ec8ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 17 Oct 2015 09:38:29 +0200 Subject: [PATCH 160/443] Hide Builds tab is GitLab CI is not enabled Signed-off-by: Dmitriy Zaporozhets --- app/helpers/projects_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index dbadbb7454..dd5e3828da 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -113,7 +113,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_build, project) + if project.gitlab_ci? && can?(current_user, :read_build, project) nav_tabs << :builds end From ca3ce5c26c60de421e010491cf9166f2090c8cc8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 17 Oct 2015 00:55:33 -0700 Subject: [PATCH 161/443] Fix nonatomic database update potentially causing project star counts to go negative The counter_cache decrement function is called when a project star is deleted, but there was no guarantee multiple workers would not attempt to delete the same item simultaneously. Use an atomic update to prevent the count from going negative. Closes #3067 --- CHANGELOG | 1 + app/models/user.rb | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a1161a7a52..2c8df90944 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.2.0 (unreleased) - Highlight comment based on anchor in URL v 8.1.0 (unreleased) + - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) diff --git a/app/models/user.rb b/app/models/user.rb index 17ccb3b878..3b346c55ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -706,12 +706,15 @@ class User < ActiveRecord::Base end def toggle_star(project) - user_star_project = users_star_projects. - where(project: project, user: self).take - if user_star_project - user_star_project.destroy - else - UsersStarProject.create!(project: project, user: self) + UsersStarProject.transaction do + user_star_project = users_star_projects. + where(project: project, user: self).lock(true).first + + if user_star_project + user_star_project.destroy + else + UsersStarProject.create!(project: project, user: self) + end end end From 748631b5a3f350fb7dc51f3ed306d27c1c3bba92 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 17 Oct 2015 10:21:01 +0200 Subject: [PATCH 162/443] Redirect old CI project route to GitLab project Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/projects_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 7777aa1803..96649ab815 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -7,6 +7,11 @@ module Ci before_action :no_cache, only: [:badge] protect_from_forgery + def show + # Temporary compatibility with CI badges pointing to CI project page + redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project) + end + # Project status badge # Image with build status for sha or ref def badge From e9be3ec8ee8fe83e1e31a832e25223e5f2603074 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 17 Oct 2015 11:05:57 +0200 Subject: [PATCH 163/443] Temporary bring /ci page page with help information Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/projects_controller.rb | 6 +++--- app/views/ci/projects/index.html.haml | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 app/views/ci/projects/index.html.haml diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 7777aa1803..b7c67b7989 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,8 +1,8 @@ module Ci class ProjectsController < Ci::ApplicationController - before_action :project - before_action :authenticate_user!, except: [:build, :badge] - before_action :authorize_access_project!, except: [:badge] + before_action :project, except: [:index] + before_action :authenticate_user!, except: [:index, :build, :badge] + before_action :authorize_access_project!, except: [:index, :badge] before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] before_action :no_cache, only: [:badge] protect_from_forgery diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml new file mode 100644 index 0000000000..9c2290bc4a --- /dev/null +++ b/app/views/ci/projects/index.html.haml @@ -0,0 +1,20 @@ +.wiki + %h1 + GitLab CI is now integrated in GitLab UI + %h2 For existing projects + + %p + Check the following pages to find the CI status you're looking for: + + %ul + %li Projects page - shows CI status for each project. + %li Project commits page - show CI status for each commit. + + + + %h2 For new projects + + %p + If you want to enable CI for a new project it is easy as adding + = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html" + file to your repository From cfa3602a1303c39e78d5bf23ad0ab34b1966d397 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:15:32 +0200 Subject: [PATCH 164/443] Move last push info at top of project page. --- app/views/projects/_activity.html.haml | 1 - app/views/projects/activity.html.haml | 2 ++ app/views/projects/show.html.haml | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index c2683bc621..012858f70b 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,4 +1,3 @@ -= render 'projects/last_push' .gray-content-block.activity-filter-block - if current_user .pull-right diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 555ed76426..69fa4ad37c 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,4 +1,6 @@ - page_title "Activity" - header_title project_title(@project, "Activity", activity_project_path(@project)) += render 'projects/last_push' + = render 'projects/activity' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index fdd3d0b322..f8c06d8b06 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,8 +7,7 @@ = render 'shared/no_ssh' = render 'shared/no_password' -- if prefer_readme? - = render 'projects/last_push' += render 'projects/last_push' = render "home_panel" From 6f0856535e242694805909b824e9e238372e3f65 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:16:04 +0200 Subject: [PATCH 165/443] Remove redundant helper method. --- app/helpers/preferences_helper.rb | 10 ++-------- app/views/projects/show.html.haml | 7 +++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 5a49ab8195..c73cb3028e 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -47,13 +47,7 @@ module PreferencesHelper Gitlab::ColorSchemes.for_user(current_user).css_class end - def prefer_readme? - !current_user || - current_user.project_view == 'readme' - end - - def current_user_default_project_view - (current_user && current_user.project_view) || - 'readme' + def default_project_view + current_user ? current_user.project_view : 'readme' end end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f8c06d8b06..585caf674c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -27,7 +27,7 @@ = link_to project_files_path(@project) do = repository_size - - if !prefer_readme? && @repository.readme + - if default_project_view != 'readme' && @repository.readme %li = link_to 'Readme', readme_path(@project) @@ -67,9 +67,8 @@ .content-block.second-block.white = render 'projects/last_commit', commit: @repository.commit, project: @project -%section - %div{class: "project-show-#{current_user_default_project_view}"} - = render current_user_default_project_view +%div{class: "project-show-#{default_project_view}"} + = render default_project_view - if current_user - access = user_max_access_in_project(current_user, @project) From 6bd9a9fbf7aa33b74ad2522466eb84fbbc6aa7b5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:16:40 +0200 Subject: [PATCH 166/443] Set vars used by tree view in project show action. --- app/controllers/projects_controller.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7158d4b49a..c81c7ea59c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,8 +1,11 @@ class ProjectsController < ApplicationController + include ExtractsPath + prepend_before_filter :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] + before_action :assign_ref_vars, :tree, only: [:show] # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] @@ -89,10 +92,7 @@ class ProjectsController < ApplicationController if current_user @membership = @project.project_member_by_id(current_user.id) end - @ref = "master" - @id = "master" - @commit = @project.repository.commit(@ref) - @tree = @project.repository.tree(@commit.id) + render :show end else @@ -228,4 +228,8 @@ class ProjectsController < ApplicationController render "go_import", layout: false end + + def get_id + project.repository.root_ref + end end From 7b26414c155796e60b4d6cff10a50d891f276ec2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:17:13 +0200 Subject: [PATCH 167/443] Split up projects/tree/_tree partial. --- ...tree.html.haml => _tree_content.html.haml} | 35 +------------------ .../projects/tree/_tree_header.html.haml | 32 +++++++++++++++++ app/views/projects/tree/show.html.haml | 8 ++--- 3 files changed, 37 insertions(+), 38 deletions(-) rename app/views/projects/tree/{_tree.html.haml => _tree_content.html.haml} (52%) create mode 100644 app/views/projects/tree/_tree_header.html.haml diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree_content.html.haml similarity index 52% rename from app/views/projects/tree/_tree.html.haml rename to app/views/projects/tree/_tree_content.html.haml index 7ff48e32e6..ed1f61e907 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,35 +1,4 @@ -.gray-content-block - %ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - tree_breadcrumbs(tree, 6) do |title, path| - %li - - if path - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - - else - = link_to title, '#' - - if allowed_tree_edit? - %li - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} - = icon('plus') - %ul.dropdown-menu - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do - = icon('pencil fw') - Create file - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do - = icon('file fw') - Upload file - %li.divider - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do - = icon('folder fw') - New directory - -%div#tree-content-holder.tree-content-holder +%div.tree-content-holder .tree-table-holder %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } %thead @@ -60,8 +29,6 @@ - if tree.readme = render "projects/tree/readme", readme: tree.readme -%div.tree_progress - - if allowed_tree_edit? = render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post = render 'projects/blob/new_dir' diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml new file mode 100644 index 0000000000..1115ca6b4c --- /dev/null +++ b/app/views/projects/tree/_tree_header.html.haml @@ -0,0 +1,32 @@ +.tree-ref-holder + = render 'shared/ref_switcher', destination: 'tree', path: @path + +%ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - tree_breadcrumbs(tree, 6) do |title, path| + %li + - if path + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) + - else + = link_to title, '#' + - if allowed_tree_edit? + %li + %span.dropdown + %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do + = icon('pencil fw') + Create file + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = icon('file fw') + Upload file + %li.divider + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = icon('folder fw') + New directory diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index dec4677f83..ec14bd7f65 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -6,12 +6,12 @@ = render 'projects/last_push' -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'tree', path: @path - - if can? current_user, :download_code, @project .tree-download-holder = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true #tree-holder.tree-holder.clearfix - = render "tree", tree: @tree + .gray-content-block.top-block + = render 'projects/tree/tree_header', tree: @tree + + = render 'projects/tree/tree_content', tree: @tree From a22fe254c18a64b1f145593781b2505d0fbdd46c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:17:31 +0200 Subject: [PATCH 168/443] Use same style for project readme, tree readme and regular blob. --- app/assets/stylesheets/pages/tree.scss | 8 -------- app/views/projects/_readme.html.haml | 15 +++++++-------- app/views/projects/tree/_readme.html.haml | 6 +++--- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index dadd86e88c..ace371d769 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -4,14 +4,6 @@ margin-right: -$gl-padding; } - .tree_progress { - display: none; - margin: 20px; - &.loading { - display: block; - } - } - .tree-table { margin-bottom: 0; diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 5bc1999ec9..0a1cecfdcd 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -1,12 +1,11 @@ - if readme = @repository.readme - %article.readme-holder#README - .clearfix - .pull-right -   - - if can?(current_user, :push_code, @project) - = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do - %i.fa-align.fa.fa-pencil - .wiki + %article.file-holder.readme-holder + .file-title + = blob_icon readme.mode, readme.name + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do + %strong + = readme.name + .file-content.wiki = cache(readme_cache_key) do = render_readme(readme) - else diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 7e9af19c8b..3c5edf4b03 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,8 +1,8 @@ -%article.file-holder.readme-holder#README +%article.file-holder.readme-holder .file-title - = link_to '#README' do + = blob_icon readme.mode, readme.name + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do %strong - %i.fa.fa-file = readme.name .file-content.wiki = render_readme(readme) From f009b6e256cbb9493883949abd87cd41920ba054 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:18:28 +0200 Subject: [PATCH 169/443] Tweak styling of project home files list. --- app/assets/stylesheets/pages/projects.scss | 6 +----- app/views/projects/_files.html.haml | 15 +++++---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index db6864ed78..bc62532f8a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -543,10 +543,6 @@ pre.light-well { } } -.project-show-files { - padding-top: 20px; -} - .project-show-readme .readme-holder { - padding: 7px; + border-top: 0; } diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 2a99708eb4..fa978325dd 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -1,11 +1,6 @@ -= render 'projects/last_push' - -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'tree', path: @path - -- if can? current_user, :download_code, @project - .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true - #tree-holder.tree-holder.clearfix - = render "projects/tree/tree", tree: @tree + .gray-content-block.second-block + = render 'projects/tree/tree_header', tree: @tree + + = render 'projects/tree/tree_content', tree: @tree + From 33fd13c8f4909cbe52a8ec38701740f11f2fccbd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:18:37 +0200 Subject: [PATCH 170/443] Remove border at bottom of readme. --- app/assets/stylesheets/framework/files.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 9dd7774788..8742d1c39b 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -10,6 +10,10 @@ border-bottom: 1px solid #E7E9EE; margin-bottom: 1em; + &.readme-holder { + border-bottom: 0; + } + table { @extend .table; } From afb33acae6ed37e630b33b7ae18d516eb8c5fdfb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:18:57 +0200 Subject: [PATCH 171/443] Put grey block around blob ref switcher and breadcrumb. --- app/views/projects/blob/_blob.html.haml | 31 ++++++++++++++----------- app/views/projects/blob/show.html.haml | 3 --- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index a1ae139758..42f632b38e 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,19 +1,22 @@ -%ul.breadcrumb.repo-breadcrumb - %li - %i.fa.fa-angle-right - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - tree_breadcrumbs(@tree, 6) do |title, path| +.gray-content-block.top-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'blob', path: @path + + %ul.breadcrumb.repo-breadcrumb %li - - if path - - if path.end_with?(@path) - = link_to namespace_project_blob_path(@project.namespace, @project, path) do - %strong - = truncate(title, length: 40) + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - tree_breadcrumbs(@tree, 6) do |title, path| + %li + - if path + - if path.end_with?(@path) + = link_to namespace_project_blob_path(@project.namespace, @project, path) do + %strong + = truncate(title, length: 40) + - else + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - - else - = link_to title, '#' + = link_to title, '#' %ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index fa4be4a1bc..f52b89f692 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -3,9 +3,6 @@ = render 'projects/last_push' -%div.tree-ref-holder - = render 'shared/ref_switcher', destination: 'blob', path: @path - %div#tree-holder.tree-holder = render 'blob', blob: @blob From 58cdfba9b9cc57998059f7ad78422d1d46b98fd7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:19:33 +0200 Subject: [PATCH 172/443] Tweak help text. --- app/views/profiles/preferences/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 01e285a8df..cc41d7dd81 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -38,7 +38,7 @@ .col-sm-10 = f.select :layout, layout_choices, {}, class: 'form-control' .help-block - Choose between fixed (max. 1200px) and fluid (100%) application layout + Choose between fixed (max. 1200px) and fluid (100%) application layout. .form-group = f.label :dashboard, class: 'control-label' do Default Dashboard @@ -52,6 +52,6 @@ .col-sm-10 = f.select :project_view, project_view_choices, {}, class: 'form-control' .help-block - Choose what content you want to see when visit project page + Choose what content you want to see on a project's home page. .panel-footer = f.submit 'Save', class: 'btn btn-save' From aebe0ddc33b93a66bd52b63a114c485791f15805 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 17 Oct 2015 19:27:02 +0200 Subject: [PATCH 173/443] Make spec names more clear --- spec/controllers/projects_controller_spec.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 590a3f5b44..b7ec5e48e8 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -25,23 +25,26 @@ describe ProjectsController do context "rendering default project view" do render_views - it "shold render the activity view", focus: true do + it "renders the activity view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('activity') + get :show, namespace_id: public_project.namespace.path, id: public_project.path expect(response).to render_template('_activity') end - it "shold render the readme view", focus: true do + it "renders the readme view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('readme') + get :show, namespace_id: public_project.namespace.path, id: public_project.path expect(response).to render_template('_readme') end - it "shold render the files view", focus: true do + it "renders the files view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('files') + get :show, namespace_id: public_project.namespace.path, id: public_project.path expect(response).to render_template('_files') end From ff866faf2fca82c7ad7e2d70cba2cae56cc5cf7f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:22:33 +0200 Subject: [PATCH 174/443] Only load tree when project has repository to prevent 404 --- app/controllers/projects_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c81c7ea59c..bb2df275b7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] - before_action :assign_ref_vars, :tree, only: [:show] + before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] @@ -229,6 +229,10 @@ class ProjectsController < ApplicationController render "go_import", layout: false end + def repo_exists? + project.repository_exists? && !project.empty_repo? + end + def get_id project.repository.root_ref end From f52b07cedcafc6cb5e92f549b7e7b4fab3b2ca83 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:22:40 +0200 Subject: [PATCH 175/443] Fix readme spec --- features/steps/project/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 15f77734cb..d76891d5bd 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -86,13 +86,13 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Forum" README' do - page.within('#README') do + page.within('.readme-holder') do expect(page).to have_content 'Sample repo for testing gitlab features' end end step 'I should see project "Shop" README' do - page.within('#README') do + page.within('.readme-holder') do expect(page).to have_content 'testme' end end From 6ad683bf13f820283a23cf44e15b241b0f4d7d87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:58:14 +0200 Subject: [PATCH 176/443] Tweak email body. --- app/views/abuse_report_mailer/notify.text.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/abuse_report_mailer/notify.text.haml b/app/views/abuse_report_mailer/notify.text.haml index 70e4e6a3c6..7dacf85703 100644 --- a/app/views/abuse_report_mailer/notify.text.haml +++ b/app/views/abuse_report_mailer/notify.text.haml @@ -1,5 +1,5 @@ -An abuse report was filed on `#{@abuse_report.user.username}` by `#{@abuse_report.reporter.username}`. +#{@abuse_report.user.name} (@#{@abuse_report.user.username}) was reported for abuse by #{@abuse_report.reporter.name} (@#{@abuse_report.reporter.username}). \ -= @abuse_report.message +> #{@abuse_report.message} \ -Abuse report admin screen: #{abuse_reports_url} \ No newline at end of file +View details: #{admin_abuse_reports_url} From dc170516edb4760d9dc8843830459fe8066dff42 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:58:24 +0200 Subject: [PATCH 177/443] Add HTML abuse report notification email. --- app/views/abuse_report_mailer/notify.html.haml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/views/abuse_report_mailer/notify.html.haml diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml new file mode 100644 index 0000000000..619533e09a --- /dev/null +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -0,0 +1,11 @@ +%p + #{link_to @abuse_report.user.name, user_url(@abuse_report.user)} + (@#{@abuse_report.user.username}) was reported for abuse by + #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} + (@#{@abuse_report.reporter.username}). + +%blockquote + = @abuse_report.message + +%p + = link_to "View details", abuse_reports_url From 9f6dc2a4b2e5eca01f5712bd7ec4d007ad4e57e5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:58:45 +0200 Subject: [PATCH 178/443] Only pass abuse report ID to AbuseReportMailer. --- app/controllers/abuse_reports_controller.rb | 7 ++++--- app/mailers/abuse_report_mailer.rb | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 482ec5054a..2f4054eaa1 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -9,11 +9,12 @@ class AbuseReportsController < ApplicationController @abuse_report.reporter = current_user if @abuse_report.save + if current_application_settings.admin_notification_email.present? + AbuseReportMailer.delay.notify(@abuse_report.id) + end + message = "Thank you for your report. A GitLab administrator will look into it shortly." redirect_to root_path, notice: message - if current_application_settings.admin_notification_email.present? - AbuseReportMailer.delay.notify(@abuse_report, current_application_settings.admin_notification_email) - end else render :new end diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index c8b9c9c162..f0c41f69a5 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,8 +1,12 @@ class AbuseReportMailer < BaseMailer + include Gitlab::CurrentSettings - def notify(abuse_report, to_email) - @abuse_report = abuse_report + def notify(abuse_report_id) + @abuse_report = AbuseReport.find(abuse_report_id) - mail(to: to_email, subject: "[Gitlab] Abuse report filed for `#{@abuse_report.user.username}`") + mail( + to: current_application_settings.admin_notification_email, + subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse" + ) end end From 3b1c702572facf3aff58beb91b2c5de4903d7a83 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:58:53 +0200 Subject: [PATCH 179/443] Fix spec. --- .../abuse_reports_controller_spec.rb | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 6d157406a2..10a2cc3c3a 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -9,45 +9,65 @@ describe AbuseReportsController do sign_in(reporter) end - describe "with admin notification_email set" do - let(:admin_email) { "admin@example.com"} - before(:example) { allow(current_application_settings).to receive(:admin_notification_email).and_return(admin_email) } + describe "POST create" do + context "with admin notification email set" do + let(:admin_email) { "admin@example.com"} - it "sends a notification email" do - post(:create, - abuse_report: { - user_id: user.id, - message: message - } - ) + before(:each) do + stub_application_setting(admin_notification_email: admin_email) + end - expect(response).to have_http_status(:redirect) - expect(flash[:notice]).to start_with("Thank you for your report") - - email = ActionMailer::Base.deliveries.last - - expect(email).to be_present - expect(email.subject).to eq("[Gitlab] Abuse report filed for `#{user.username}`") - expect(email.to).to eq([admin_email]) - expect(email.body).to include(message) - end - end - - describe "without admin notification email set" do - before(:example) { allow(current_application_settings).to receive(:admin_notification_email).and_return(nil) } - - it "does not send a notification email" do - expect do - post(:create, + it "sends a notification email" do + post :create, abuse_report: { user_id: user.id, message: message } - ) - end.to_not change{ActionMailer::Base.deliveries} - expect(response).to have_http_status(:redirect) - expect(flash[:notice]).to start_with("Thank you for your report") + email = ActionMailer::Base.deliveries.last + + expect(email.to).to eq([admin_email]) + expect(email.subject).to include(user.username) + expect(email.text_part.body).to include(message) + end + + it "saves the abuse report" do + expect { + post :create, + abuse_report: { + user_id: user.id, + message: message + } + }.to change { AbuseReport.count }.by(1) + end + end + + context "without admin notification email set" do + before(:each) do + stub_application_setting(admin_notification_email: nil) + end + + it "does not send a notification email" do + expect { + post :create, + abuse_report: { + user_id: user.id, + message: message + } + + }.not_to change { ActionMailer::Base.deliveries.count } + end + + it "saves the abuse report" do + expect { + post :create, + abuse_report: { + user_id: user.id, + message: message + } + }.to change { AbuseReport.count }.by(1) + end end end -end \ No newline at end of file + +end From 551512b147f63a9fcab938603eb70c112e80fee7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:58:59 +0200 Subject: [PATCH 180/443] Validate admin notification email. --- app/models/application_setting.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c8841178e9..05430c2ee1 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -44,6 +44,10 @@ class ApplicationSetting < ActiveRecord::Base allow_blank: true, format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates :admin_notification_email, + allow_blank: true, + email: true + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| From 7ccfbcccef9a78f9dd469b22070663028ff0e972 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 11:59:07 +0200 Subject: [PATCH 181/443] Add help text to admin settings notification email. --- app/views/admin/application_settings/_form.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 791734dd80..7a78526e09 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -51,6 +51,8 @@ = f.label :admin_notification_email, class: 'control-label col-sm-2' .col-sm-10 = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. %fieldset %legend Account and Limit Settings From 024b6fa11a3d0d4aed017997148524e0df1eb177 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:06:47 +0200 Subject: [PATCH 182/443] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 8f17c5c2ba..b18a08bf89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.2.0 (unreleased) - Highlight comment based on anchor in URL v 8.1.0 (unreleased) + - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x From 99b8568ff79b188d664de9744797ce4013e55526 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:19:30 +0200 Subject: [PATCH 183/443] Find correct group membership. --- app/controllers/projects_controller.rb | 3 +-- app/views/projects/buttons/_notifications.html.haml | 11 +++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ffbd91324c..1ea992c4e8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -87,8 +87,7 @@ class ProjectsController < ApplicationController render 'projects/empty' else if current_user - @membership = @project.project_member_by_id(current_user.id) - @group_member = GroupMember.find_by(user_id: current_user.id) + @membership = @project.team.find_member(current_user.id) end render :show diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 6a620e7c23..9783ff8431 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,6 +1,5 @@ -- return unless [@membership, @group_member].any? - -- if @membership +- case @membership +- when ProjectMember = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do = hidden_field_tag :notification_type, 'project' = hidden_field_tag :notification_id, @membership.id @@ -14,8 +13,8 @@ - Notification.project_notification_levels.each do |level| = notification_list_item(level, @membership) -- elsif @group_member - .btn.btn-new.disabled#notifications-button +- when GroupMember + .btn.btn-new.disabled = icon('bell') - = notification_label(@group_member) + = notification_label(@membership) = icon('angle-down') From 1195ecd1985494704fcbd859078e5ba99182ece0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:19:36 +0200 Subject: [PATCH 184/443] Add tooltip. --- app/views/projects/buttons/_notifications.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 9783ff8431..0c29884491 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -14,7 +14,7 @@ = notification_list_item(level, @membership) - when GroupMember - .btn.btn-new.disabled + .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} = icon('bell') = notification_label(@membership) = icon('angle-down') From eeea6ef25f222e3935ef4a86e59d982ba6758b9a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:19:42 +0200 Subject: [PATCH 185/443] Sentences end in periods. --- app/views/shared/_clone_panel.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index b23b2f0d5e..2e4aab3630 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -6,7 +6,7 @@ type: 'button', | class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | :"data-clone" => project.ssh_url_to_repo, | - :"data-title" => "Add an SSH key to your profile
to pull or push via SSH", + :"data-title" => "Add an SSH key to your profile
to pull or push via SSH.", :"data-html" => "true", :"data-container" => "body"} SSH @@ -15,7 +15,7 @@ type: 'button', | class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | :"data-clone" => project.http_url_to_repo, | - :"data-title" => "Set a password on your account
to pull or push via #{gitlab_config.protocol.upcase}", + :"data-title" => "Set a password on your account
to pull or push via #{gitlab_config.protocol.upcase}.", :"data-html" => "true", :"data-container" => "body"} = gitlab_config.protocol.upcase From ef9284636cbc63a7b6a8f8ddeabb1152ad4f6a96 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:21:28 +0200 Subject: [PATCH 186/443] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 8f17c5c2ba..2e86724b25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.2.0 (unreleased) - Highlight comment based on anchor in URL v 8.1.0 (unreleased) + - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x From 42cbc7f813386dbf6d28868c9972ff38f01ad095 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:37:50 +0200 Subject: [PATCH 187/443] Tweak wording. --- app/controllers/projects_controller.rb | 4 ++- app/helpers/projects_helper.rb | 2 +- app/views/projects/edit.html.haml | 30 ++++++++++---------- spec/controllers/projects_controller_spec.rb | 4 +-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 77b3af9a5d..12ef073e14 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -71,7 +71,7 @@ class ProjectsController < ApplicationController if @project.forked? @project.forked_project_link.destroy - flash[:notice] = 'Fork relationship has been removed.' + flash[:notice] = 'The fork relationship has been removed.' end end @@ -150,6 +150,7 @@ class ProjectsController < ApplicationController def archive return access_denied! unless can?(current_user, :archive_project, @project) + @project.archive! respond_to do |format| @@ -159,6 +160,7 @@ class ProjectsController < ApplicationController def unarchive return access_denied! unless can?(current_user, :archive_project, @project) + @project.unarchive! respond_to do |format| diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e6e1bfd2c9..472884e4ff 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -71,7 +71,7 @@ module ProjectsHelper end def remove_fork_project_message(project) - "You are going to remove the fork relationship to the source project from #{@project.forked_from_project.namespace.try(:name)}. Are you ABSOLUTELY sure?" + "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?" end def project_nav_tabs diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ec58d0924b..afbf88b550 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -189,20 +189,20 @@ - else .nothing-here-block Only the project owner can transfer a project - - if @project.forked? && can?(current_user, :remove_fork_project, @project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .panel.panel-default.panel.panel-danger - .panel-heading Remove fork relationship - .panel-body - %p - This will remove the relationship to the source project from - = link_to project_path(@project.forked_from_project) do - = @project.forked_from_project.namespace.try(:name) - %br - %strong Once removed it cannot be reversed through this interface. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - - elsif @project.forked? - .nothing-here-block Only the project owner can remove the fork relationship + - if @project.forked? + - if can?(current_user, :remove_fork_project, @project) + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| + .panel.panel-default.panel.panel-danger + .panel-heading Remove fork relationship + .panel-body + %p + This will remove the fork relationship to source project + #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}. + %br + %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - else + .nothing-here-block Only the project owner can remove the fork relationship. - if can?(current_user, :remove_project, @project) .panel.panel-default.panel.panel-danger @@ -216,7 +216,7 @@ = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - else - .nothing-here-block Only project owner can remove a project + .nothing-here-block Only the project owner can remove a project. .save-project-loader.hide diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e963e91351..9b0527a68d 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -63,7 +63,7 @@ describe ProjectsController do end end - describe "PUT remove_fork" do + describe "DELETE remove_fork" do context 'when signed in' do before do sign_in(user) @@ -82,7 +82,7 @@ describe ProjectsController do id: project_fork.to_param, format: :js) expect(project_fork.forked?).to be_falsey - expect(flash[:notice]).to eq('Fork relationship has been removed.') + expect(flash[:notice]).to eq('The fork relationship has been removed.') expect(response).to render_template(:remove_fork) end end From b371b6d13974fe8db3601e68922db0b735049a17 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 12:54:02 +0200 Subject: [PATCH 188/443] Update style of snippet detail page --- CHANGELOG | 2 +- app/assets/stylesheets/pages/snippets.scss | 66 ++++++++++----------- app/views/projects/snippets/show.html.haml | 25 ++++---- app/views/shared/snippets/_header.html.haml | 47 +++++++-------- app/views/snippets/show.html.haml | 22 +++---- 5 files changed, 83 insertions(+), 79 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8b225d264e..df4daddeac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,7 +59,7 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) - - Added last modified date to snippets#show (Han Loong Liauw) + - Update style of snippet detail page (Han Loong Liauw) - Allow dashboard and group issues/MRs to be filtered by label - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index af39148176..f8a8636818 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -31,50 +31,50 @@ } } -.snippet-details { - .page-title { - margin-top: -15px; - padding: 10px 0; - margin-bottom: 0; - color: #5c5d5e; - font-size: 13px; - @include clearfix(); +.snippet-holder { + .snippet-details { + .page-title { + margin-top: -15px; + padding: 10px 0; + margin-bottom: 0; + color: #5c5d5e; + font-size: 16px; - .creator { - color: $gl-gray; - a { - color: $gl-gray; + .author { + color: #5c5d5e; + } + + .snippet-id { + color: #5c5d5e; } } - .snippet-id { - color: #5c5d5e; + .snippet-title { + margin: 0; + font-size: 23px; + color: #313236; } - .btn { - padding: 10px $gl-padding; + + @media (max-width: $screen-md-max) { + .new-snippet-link { + display: none; + } + } + + @media (max-width: $screen-sm-max) { + .creator, + .page-title .btn-close { + display: none; + } } } - .snippet-title { - margin: 0; - font-size: 23px; - color: #313236; - } - - @media (max-width: $screen-md-max) { - .new-snippet-link { - display: none; - } - } - - @media (max-width: $screen-sm-max) { - .creator, - .page-title .btn-close { - display: none; - } + .file-holder { + border-top: 0; } } + .snippet-box { @include border-radius(2px); diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 1aeb0da201..5d706942f2 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,15 +1,18 @@ - page_title @snippet.title, "Snippets" = render "header_title" -= render 'shared/snippets/header' -.file-holder - .file-title - %i.fa.fa-file - %strong - = @snippet.file_name - .file-actions - .btn-group - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" +.snippet-holder + = render 'shared/snippets/header' - = render 'shared/snippets/blob' -%div#notes= render "projects/notes/notes_with_form" + %article.file-holder + .file-title + = blob_icon 0, @snippet.file_name + %strong + = @snippet.file_name + .file-actions.hidden-xs + .btn-group.tree-btn-group + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + + = render 'shared/snippets/blob' + + %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 95786ee337..0a4a790ec5 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,25 +1,24 @@ -.snippet - .snippet-details - .page-title - .snippet-box{class: visibility_level_color(@snippet.visibility_level)} - = visibility_level_icon(@snippet.visibility_level) - = visibility_level_label(@snippet.visibility_level) - %span.snippet-id Snippet ##{@snippet.id} - %span.creator - · created by #{link_to_member(@project, @snippet.author, size: 24)} - · - = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') +.snippet-details + .page-title + .snippet-box{class: visibility_level_color(@snippet.visibility_level)} + = visibility_level_icon(@snippet.visibility_level) + = visibility_level_label(@snippet.visibility_level) + %span.snippet-id Snippet ##{@snippet.id} + %span.creator + · created by #{link_to_member(@project, @snippet.author, size: 24)} + · + = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + - if @snippet.updated_at != @snippet.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') - .pull-right - - if @snippet.project_id? - = render "projects/snippets/actions" - - else - = render "snippets/actions" - .gray-content-block.middle-block - %h2.snippet-title - = gfm escape_once(@snippet.title) + .pull-right + - if @snippet.project_id? + = render "projects/snippets/actions" + - else + = render "snippets/actions" + .gray-content-block.middle-block + %h2.snippet-title + = gfm escape_once(@snippet.title) diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 612d7b65d8..69d8899d4c 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,12 +1,14 @@ - page_title @snippet.title, "Snippets" -= render 'shared/snippets/header' -.file-holder - .file-title - %i.fa.fa-file - %strong - = @snippet.file_name - .file-actions - .btn-group - = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' +.snippet-holder + = render 'shared/snippets/header' + + %article.file-holder + .file-title + = blob_icon 0, @snippet.file_name + %strong + = @snippet.file_name + .file-actions.hidden-xs + .btn-group.tree-btn-group + = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' From 946f00ed7f2b487273bb5dabdb5997da60f1dc92 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 13:03:26 +0200 Subject: [PATCH 189/443] Update style of snippets pages --- CHANGELOG | 2 +- app/assets/stylesheets/pages/snippets.scss | 5 ---- .../projects/snippets_controller.rb | 1 + app/views/dashboard/snippets/index.html.haml | 28 ++----------------- app/views/explore/snippets/index.html.haml | 3 +- app/views/projects/snippets/index.html.haml | 20 ++++++------- 6 files changed, 15 insertions(+), 44 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index df4daddeac..557216879f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,7 +59,7 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) - - Update style of snippet detail page (Han Loong Liauw) + - Update style of snippets pages (Han Loong Liauw) - Allow dashboard and group issues/MRs to be filtered by label - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index f8a8636818..242783a7b7 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -1,8 +1,3 @@ -.my-snippets li:first-child { - h4 { margin-top: 0; } - padding-top: 0; -} - .snippet-form-holder .file-holder .file-title { padding: 2px; } diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b07a2a8db2..2104c7a7a7 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -21,6 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController filter: :by_project, project: @project }) + @snippets = @snippets.page(params[:page]).per(PER_PAGE) end def new diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index d3908062f4..d92149dcc7 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -6,33 +6,11 @@ .gray-content-block .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet + = icon('plus') + New Snippet .oneline Share code pastes with others out of git repository -%ul.nav.nav-tabs.prepend-top-20 - = nav_tab :scope, nil do - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count - -.my-snippets - = render 'snippets/snippets' += render 'snippets/snippets' diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7e4fa7d487..0f100c39ff 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -10,7 +10,8 @@ - if current_user .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet + = icon('plus') + New Snippet .oneline Public snippets created by you and other users are listed here diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 3fed2c9949..4af963e14d 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,17 +1,13 @@ - page_title "Snippets" = render "header_title" -%h3.page-title - Snippets - - if can? current_user, :create_project_snippet, @project - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do - Add new snippet +.gray-content-block.top-block + .pull-right + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do + = icon('plus') + New Snippet -%p.light - Share code pastes with others out of git repository + .oneline + Share code pastes with others out of git repository -%ul.bordered-list - = render partial: "shared/snippets/snippet", collection: @snippets - - if @snippets.empty? - %li - .nothing-here-block Nothing here. += render 'snippets/snippets' From 2e2a2a366fa5a7b1179bf34bf22128138e52e4c7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 13:08:08 +0200 Subject: [PATCH 190/443] Satisfy Rubocop --- spec/controllers/abuse_reports_controller_spec.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 10a2cc3c3a..0faab8d7ff 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -32,13 +32,13 @@ describe AbuseReportsController do end it "saves the abuse report" do - expect { + expect do post :create, abuse_report: { user_id: user.id, message: message } - }.to change { AbuseReport.count }.by(1) + end.to change { AbuseReport.count }.by(1) end end @@ -48,24 +48,23 @@ describe AbuseReportsController do end it "does not send a notification email" do - expect { + expect do post :create, abuse_report: { user_id: user.id, message: message } - - }.not_to change { ActionMailer::Base.deliveries.count } + end.not_to change { ActionMailer::Base.deliveries.count } end it "saves the abuse report" do - expect { + expect do post :create, abuse_report: { user_id: user.id, message: message } - }.to change { AbuseReport.count }.by(1) + end.to change { AbuseReport.count }.by(1) end end end From cc2b05adf80f55c9f0fe4b453f9f45e2b402c006 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 14:05:27 +0200 Subject: [PATCH 191/443] Fix bug where a push would only create cross references from the first commit. --- app/services/git_push_service.rb | 2 +- lib/gitlab/reference_extractor.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e54044365b..3de7bb9dca 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -79,7 +79,7 @@ class GitPushService authors = Hash.new do |hash, commit| email = commit.author_email - return hash[email] if hash.has_key?(email) + next hash[email] if hash.has_key?(email) hash[email] = commit_user(commit) end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 333bd05905..da8df8a302 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -27,7 +27,7 @@ module Gitlab def references @references ||= Hash.new do |references, type| type = type.to_sym - return references[type] if references.has_key?(type) + next references[type] if references.has_key?(type) references[type] = pipeline_result(type) end From f3a74556b15f7429749384a823b73253602454cf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 14:12:50 +0200 Subject: [PATCH 192/443] Fix spec --- spec/features/projects_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index f3d51641ec..09fcff2444 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -49,7 +49,7 @@ feature 'Project', feature: true do remove_with_confirm('Remove fork relationship', project.path) - expect(page).to have_content 'Fork relationship has been removed.' + expect(page).to have_content 'The fork relationship has been removed.' expect(project.forked?).to be_falsey expect(page).not_to have_content 'Remove fork relationship' end From 44df201280ce67ac704c31e7cc24d5c6b34cff90 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 14:17:15 +0200 Subject: [PATCH 193/443] Restore dashboard snippets tabs. --- app/views/dashboard/snippets/index.html.haml | 22 ++++++++++++++++++++ features/steps/snippets/user.rb | 6 +++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index d92149dcc7..7ac3a12baf 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -12,5 +12,27 @@ .oneline Share code pastes with others out of git repository +%ul.center-top-menu.no-top.no-bottom.snippet-scope-menu + = nav_tab :scope, nil do + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + = nav_tab :scope, 'are_private' do + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + = nav_tab :scope, 'are_internal' do + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + = nav_tab :scope, 'are_public' do + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count + = render 'snippets/snippets' diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index dea3256229..997c605bce 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -32,19 +32,19 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps end step 'I click "Internal" filter' do - page.within('.nav-tabs') do + page.within('.snippet-scope-menu') do click_link "Internal" end end step 'I click "Private" filter' do - page.within('.nav-tabs') do + page.within('.snippet-scope-menu') do click_link "Private" end end step 'I click "Public" filter' do - page.within('.nav-tabs') do + page.within('.snippet-scope-menu') do click_link "Public" end end From aafb36616cbad9b3478964dffc10fca97c2f55bb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 14:26:25 +0200 Subject: [PATCH 194/443] Fix schema [ci skip] --- db/schema.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 7d60e3cf9e..886b05f3e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -416,7 +416,6 @@ ActiveRecord::Schema.define(version: 20151016195706) do end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree - add_index "labels", ["title"], name: "index_labels_on_title", using: :btree create_table "members", force: true do |t| t.integer "access_level", null: false @@ -498,7 +497,6 @@ ActiveRecord::Schema.define(version: 20151016195706) do add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree - add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree create_table "namespaces", force: true do |t| t.string "name", null: false From d12e49c5916a42dafba32555405878f32c7ad254 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 15:07:02 +0200 Subject: [PATCH 195/443] Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. --- CHANGELOG | 1 + app/services/merge_requests/post_merge_service.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index abf9ead6df..699b44f00d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.2.0 (unreleased) - Highlight comment based on anchor in URL v 8.1.0 (unreleased) + - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index aceb8cb902..8f25c5e249 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -6,6 +6,7 @@ module MergeRequests # class PostMergeService < MergeRequests::BaseService def execute(merge_request) + close_issues(merge_request) merge_request.mark_as_merged create_merge_event(merge_request, current_user) create_note(merge_request) @@ -15,6 +16,15 @@ module MergeRequests private + def close_issues(merge_request) + return unless merge_request.target_branch == project.default_branch + + closed_issues = merge_request.closes_issues(current_user) + closed_issues.each do |issue| + Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + end + end + def create_merge_event(merge_request, current_user) EventCreateService.new.merge_mr(merge_request, current_user) end From 85264ab6a9fa11a2e325d72c02c00d4b8862f933 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 15:24:04 +0200 Subject: [PATCH 196/443] Restore notification footer text. --- app/views/layouts/notify.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 2f7d7e86f5..854cda57c3 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -41,4 +41,8 @@ #{link_to "view it on GitLab", @target_url}. - else #{link_to "View it on GitLab", @target_url} + %br + You're receiving this email because of your account on #{link_to Gitlab.config.gitlab.host, root_url}. + If you'd like to receive fewer emails, you can adjust your notification settings. + = email_action @target_url From d9944fdb59fe286022130f133f316b80dec04ea6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 18 Oct 2015 15:36:02 +0200 Subject: [PATCH 197/443] Fix spec --- features/steps/project/snippets.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 811ded6955..a3aef9bf8c 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New Snippet"' do - click_link "Add new snippet" + click_link "New Snippet" end step 'I click link "Snippet one"' do From 405a10e9b8f94578b0417cb4fea0362aa139bac3 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 18 Oct 2015 17:42:18 +0100 Subject: [PATCH 198/443] Fix spelling of Description --- app/views/projects/ci_services/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml index c78b21884a..c164b2d4bc 100644 --- a/app/views/projects/ci_services/index.html.haml +++ b/app/views/projects/ci_services/index.html.haml @@ -6,7 +6,7 @@ %tr %th %th Service - %th Desription + %th Description %th Last edit - @services.sort_by(&:title).each do |service| %tr From a32f7766098bf38b1028168b4919516460a562e9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 19 Oct 2015 11:19:45 +0200 Subject: [PATCH 199/443] Make tables full width. --- app/assets/stylesheets/framework/files.scss | 1 - app/assets/stylesheets/framework/lists.scss | 2 +- app/assets/stylesheets/framework/tables.scss | 10 +- .../stylesheets/framework/timeline.scss | 2 +- app/assets/stylesheets/pages/ci_projects.scss | 5 - app/assets/stylesheets/pages/events.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 7 -- app/views/admin/abuse_reports/index.html.haml | 21 ++-- app/views/admin/applications/show.html.haml | 37 +++---- .../admin/background_jobs/show.html.haml | 37 +++---- app/views/admin/deploy_keys/index.html.haml | 37 +++---- app/views/admin/identities/index.html.haml | 15 +-- app/views/admin/services/index.html.haml | 39 +++---- app/views/ci/admin/events/index.html.haml | 33 +++--- app/views/ci/admin/projects/index.html.haml | 23 ++-- app/views/ci/admin/runners/index.html.haml | 27 ++--- app/views/ci/events/index.html.haml | 33 +++--- app/views/ci/lints/_create.html.haml | 45 ++++---- app/views/dashboard/milestones/show.html.haml | 42 ++++---- .../doorkeeper/applications/index.html.haml | 30 +++--- .../doorkeeper/applications/show.html.haml | 38 +++---- .../authorized_applications/index.html.haml | 25 ++--- app/views/groups/milestones/show.html.haml | 42 ++++---- app/views/import/bitbucket/status.html.haml | 77 ++++++------- .../import/fogbugz/new_user_map.html.haml | 31 +++--- app/views/import/fogbugz/status.html.haml | 63 +++++------ app/views/import/github/status.html.haml | 63 +++++------ app/views/import/gitlab/status.html.haml | 63 +++++------ app/views/import/gitorious/status.html.haml | 63 +++++------ app/views/import/google_code/status.html.haml | 77 ++++++------- app/views/profiles/_event_table.html.haml | 29 ++--- app/views/projects/builds/index.html.haml | 29 ++--- .../projects/ci_web_hooks/index.html.haml | 23 ++-- app/views/projects/commit/ci.html.haml | 48 +++++---- .../_branches_list.html.haml | 59 +++++----- app/views/projects/runners/show.html.haml | 101 +++++++++--------- app/views/projects/services/index.html.haml | 39 +++---- app/views/projects/tree/_tree.html.haml | 2 +- app/views/projects/triggers/index.html.haml | 13 +-- app/views/projects/wikis/history.html.haml | 49 ++++----- 41 files changed, 707 insertions(+), 677 deletions(-) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 9dd7774788..c2118abbf5 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -94,7 +94,6 @@ border-right: none; } background: #fff; - padding: 10px $gl-padding; } .lines { pre { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index c5764c3659..f6942db581 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -107,7 +107,7 @@ ul.content-list { > li { padding: $gl-padding; - border-color: #f1f2f4; + border-color: $table-border-color; margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 789b34020c..66e16e8df7 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,3 +1,9 @@ +.table-holder { + margin: -$gl-padding; + margin-top: 0; + margin-bottom: 0; +} + table { &.table { .dropdown-menu a { @@ -18,15 +24,17 @@ table { tr { td, th { - padding: 8px 10px; + padding: 10px $gl-padding; line-height: 20px; vertical-align: middle; } + th { font-weight: normal; font-size: 15px; border-bottom: 1px solid $border-color !important; } + td { border-color: $table-border-color !important; border-bottom: 1px solid; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 9d6f053aef..eb53c4153d 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -6,7 +6,7 @@ .timeline-entry { padding: $gl-padding; - border-color: #f1f2f4; + border-color: $table-border-color; margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 8c5273abcd..2a7b5cfc7f 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -6,11 +6,6 @@ line-height: 1.5; } - .wide-table-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - } - .builds, .projects-table { .light { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index ca2ee45542..dfb901652b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -7,7 +7,7 @@ padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-bottom: 1px solid #f1f2f4; + border-bottom: 1px solid $table-border-color; color: #7f8fa4; &.event-inline { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 48b8775026..a27763ad8f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -457,7 +457,7 @@ pre.light-well { .project-row { padding: $gl-padding; - border-color: #f1f2f4; + border-color: $table-border-color; margin-left: -$gl-padding; margin-right: -$gl-padding; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index dadd86e88c..b19b710472 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,9 +1,4 @@ .tree-holder { - .tree-table-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - } - .tree_progress { display: none; margin: 20px; @@ -17,9 +12,7 @@ tr { > td, > th { - padding: 10px $gl-padding; line-height: 32px; - border-color: $table-border-color !important; } &:hover { diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 2e8746146d..40a5fe4628 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -2,16 +2,17 @@ %h3.page-title Abuse Reports %hr - if @abuse_reports.present? - %table.table - %thead - %tr - %th Reported by - %th Reported at - %th Message - %th User - %th Primary action - %th - = render @abuse_reports + .table-holder + %table.table + %thead + %tr + %th Reported by + %th Reported at + %th Message + %th User + %th Primary action + %th + = render @abuse_reports = paginate @abuse_reports - else %h4 There are no abuse reports diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 0ea2ffeda9..3eb9d61972 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -3,25 +3,26 @@ Application: #{@application.name} -%table.table - %tr - %td - Application Id - %td - %code#application_id= @application.uid - %tr - %td - Secret: - %td - %code#secret= @application.secret +.table-holder + %table.table + %tr + %td + Application Id + %td + %code#application_id= @application.uid + %tr + %td + Secret: + %td + %code#secret= @application.secret - %tr - %td - Callback url - %td - - @application.redirect_uri.split.each do |uri| - %div - %span.monospace= uri + %tr + %td + Callback url + %td + - @application.redirect_uri.split.each do |uri| + %div + %span.monospace= uri .form-actions = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 3a01e11510..de5bc050cf 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -12,24 +12,25 @@ %i.fa.fa-exclamation-triangle There are no running sidekiq processes. Please restart GitLab - else - %table.table - %thead - %th USER - %th PID - %th CPU - %th MEM - %th STATE - %th START - %th COMMAND - %tbody - - @sidekiq_processes.each do |process| - - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) - - data = process.strip.split(' ') - %tr - %td= gitlab_config.user - - 5.times do - %td= data.shift - %td= data.join(' ') + .table-holder + %table.table + %thead + %th USER + %th PID + %th CPU + %th MEM + %th STATE + %th START + %th COMMAND + %tbody + - @sidekiq_processes.each do |process| + - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) + - data = process.strip.split(' ') + %tr + %td= gitlab_config.user + - 5.times do + %td= data.shift + %td= data.join(' ') .clearfix %p diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 2bf1689cbc..841e6971fb 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -5,22 +5,23 @@ .panel-head-actions = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" - if @deploy_keys.any? - %table.table - %thead.panel-heading - %tr - %th Title - %th Fingerprint - %th Added at - %th - %tbody - - @deploy_keys.each do |deploy_key| + .table-holder + %table.table + %thead.panel-heading %tr - %td - %strong= deploy_key.title - %td - %code.key-fingerprint= deploy_key.fingerprint - %td - %span.cgray - added #{time_ago_with_tooltip(deploy_key.created_at)} - %td - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" + %th Title + %th Fingerprint + %th Added at + %th + %tbody + - @deploy_keys.each do |deploy_key| + %tr + %td + %strong= deploy_key.title + %td + %code.key-fingerprint= deploy_key.fingerprint + %td + %span.cgray + added #{time_ago_with_tooltip(deploy_key.created_at)} + %td + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index ae57e3adc4..8358a14445 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -2,12 +2,13 @@ = render 'admin/users/head' - if @identities.present? - %table.table - %thead - %tr - %th Provider - %th Identifier - %th - = render @identities + .table-holder + %table.table + %thead + %tr + %th Provider + %th Identifier + %th + = render @identities - else %h4 This user has no identities diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml index e237729114..6a5986f496 100644 --- a/app/views/admin/services/index.html.haml +++ b/app/views/admin/services/index.html.haml @@ -2,22 +2,23 @@ %h3.page-title Service templates %p.light Service template allows you to set default values for project services -%table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = icon("copy", class: 'clgray') - %td - = link_to edit_admin_application_settings_service_path(service.id) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago +.table-holder + %table.table + %thead + %tr + %th + %th Service + %th Description + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = icon("copy", class: 'clgray') + %td + = link_to edit_admin_application_settings_service_path(service.id) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml index f9ab099430..5a5b4dc7c3 100644 --- a/app/views/ci/admin/events/index.html.haml +++ b/app/views/ci/admin/events/index.html.haml @@ -1,17 +1,18 @@ -%table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago +.table-holder + %table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago -= paginate @events \ No newline at end of file += paginate @events diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml index dc7b041473..0da8547924 100644 --- a/app/views/ci/admin/projects/index.html.haml +++ b/app/views/ci/admin/projects/index.html.haml @@ -1,15 +1,16 @@ -%table.table - %thead - %tr - %th ID - %th Name - %th Last build - %th Access - %th Builds - %th +.table-holder + %table.table + %thead + %tr + %th ID + %th Name + %th Last build + %th Access + %th Builds + %th - - @projects.each do |project| - = render "ci/admin/projects/project", project: project + - @projects.each do |project| + = render "ci/admin/projects/project", project: project = paginate @projects diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index 01ce81b447..bb213fbffc 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -35,18 +35,19 @@ %br -%table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Projects - %th Builds - %th Tags - %th Last contact - %th +.table-holder + %table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Projects + %th Builds + %th Tags + %th Last contact + %th - - @runners.each do |runner| - = render "ci/admin/runners/runner", runner: runner + - @runners.each do |runner| + = render "ci/admin/runners/runner", runner: runner = paginate @runners diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml index 779f49b3d3..9824e85b1a 100644 --- a/app/views/ci/events/index.html.haml +++ b/app/views/ci/events/index.html.haml @@ -1,19 +1,20 @@ %h3.page-title Events -%table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago +.table-holder + %table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago -= paginate @events \ No newline at end of file += paginate @events diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index e2179e60f3..f45cd05aec 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -4,29 +4,30 @@ syntax is correct %i.fa.fa-ok.correct-syntax - %table.table.table-bordered - %thead - %tr - %th Parameter - %th Value - %tbody - - @stages.each do |stage| - - @builds.select { |build| build[:stage] == stage }.each do |build| - %tr - %td #{stage.capitalize} Job - #{build[:name]} - %td - %pre - = simple_format build[:script] + .table-holder + %table.table.table-bordered + %thead + %tr + %th Parameter + %th Value + %tbody + - @stages.each do |stage| + - @builds.select { |build| build[:stage] == stage }.each do |build| + %tr + %td #{stage.capitalize} Job - #{build[:name]} + %td + %pre + = simple_format build[:script] - %br - %b Tag list: - = build[:tags] - %br - %b Refs only: - = build[:only] && build[:only].join(", ") - %br - %b Refs except: - = build[:except] && build[:except].join(", ") + %br + %b Tag list: + = build[:tags] + %br + %b Refs only: + = build[:only] && build[:only].join(", ") + %br + %b Refs except: + = build[:except] && build[:except].join(", ") -else %p diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index d5c4a44fef..2fe14c6388 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -13,26 +13,28 @@ %span All issues for this milestone are closed. You may close the milestone now. .description -%table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @dashboard_milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at + +.table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @dashboard_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td + - if milestone.closed? + Closed + - else + Open + %td + = milestone.expires_at .context %p.lead diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 3b0b19107c..ba4c5b86ef 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,17 +1,19 @@ - page_title "Applications" %h3.page-title Your applications %p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' -%table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td= application.redirect_uri - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link' - %td= render 'delete_form', application: application + +.table-holder + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td= application.redirect_uri + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link' + %td= render 'delete_form', application: application diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 80340aca54..47442b78d4 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -2,26 +2,26 @@ %h3.page-title Application: #{@application.name} +.table-holder + %table.table + %tr + %td + Application Id + %td + %code#application_id= @application.uid + %tr + %td + Secret: + %td + %code#secret= @application.secret -%table.table - %tr - %td - Application Id - %td - %code#application_id= @application.uid - %tr - %td - Secret: - %td - %code#secret= @application.secret - - %tr - %td - Callback url - %td - - @application.redirect_uri.split.each do |uri| - %div - %span.monospace= uri + %tr + %td + Callback url + %td + - @application.redirect_uri.split.each do |uri| + %div + %span.monospace= uri .form-actions = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml index 814cdc987e..b184b9c01d 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.haml +++ b/app/views/doorkeeper/authorized_applications/index.html.haml @@ -1,16 +1,17 @@ %header.page-header %h1 Your authorized applications %main{:role => "main"} - %table.table.table-striped - %thead - %tr - %th Application - %th Created At - %th - %th - %tbody - - @applications.each do |application| + .table-holder + %table.table.table-striped + %thead %tr - %td= application.name - %td= application.created_at.strftime('%Y-%m-%d %H:%M:%S') - %td= render 'delete_form', application: application \ No newline at end of file + %th Application + %th Created At + %th + %th + %tbody + - @applications.each do |application| + %tr + %td= application.name + %td= application.created_at.strftime('%Y-%m-%d %H:%M:%S') + %td= render 'delete_form', application: application diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index c6cde97c58..a92ad5d751 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -21,26 +21,28 @@ %span All issues for this milestone are closed. You may close the milestone now. .description -%table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @group_milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at + +.table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @group_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td + - if milestone.closed? + Closed + - else + Open + %td + = milestone.expires_at .context %p.lead diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 777eb48271..30bcdb8682 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -14,45 +14,46 @@ = button_tag 'Import all projects', class: "btn btn-success js-import-all" -%table.table.import-jobs - %thead - %tr - %th From Bitbucket - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From Bitbucket + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} - %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" - %td.import-target - = "#{repo["owner"]}/#{repo["slug"]}" - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" - - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} - %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" - %td.import-target - %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + - @repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + %td.import-target + = "#{repo["owner"]}/#{repo["slug"]}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" - if @incompatible_repos.any? %p diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 25cebfb366..a701e49ac5 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -25,22 +25,23 @@ of issues and comments (e.g. "By @johnsmith"). It will also associate and/or assign these issues and comments with the selected user. - %table.table - %thead - %tr - %th ID - %th Name - %th Email - %th GitLab User - %tbody - - @user_map.each do |id, user| + .table-holder + %table.table + %thead %tr - %td= id - %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' - %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' - %td - = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control', - scope: :all, email_user: true, selected: user[:gitlab_user]) + %th ID + %th Name + %th Email + %th GitLab User + %tbody + - @user_map.each do |id, user| + %tr + %td= id + %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' + %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' + %td + = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control', + scope: :all, email_user: true, selected: user[:gitlab_user]) .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index f179ece402..beca6ab142 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -14,38 +14,39 @@ %p = button_tag 'Import all projects', class: 'btn btn-success js-import-all' -%table.table.import-jobs - %thead - %tr - %th From FogBugz - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = project.import_source - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From FogBugz + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = project.import_source + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = repo.name - %td.import-target - = "#{current_user.username}/#{repo.name}" - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = repo.name + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" :coffeescript new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}") diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index ef55249823..0669b05adc 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -9,38 +9,39 @@ %p = button_tag 'Import all projects', class: "btn btn-success js-import-all" -%table.table.import-jobs - %thead - %tr - %th From GitHub - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From GitHub + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" - %td.import-target - = repo.full_name - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" :coffeescript new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 727f3c7e7f..3bc85059e7 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -9,38 +9,39 @@ %p = button_tag 'Import all projects', class: "btn btn-success js-import-all" -%table.table.import-jobs - %thead - %tr - %th From GitLab.com - %th To this GitLab instance - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From GitLab.com + %th To this GitLab instance + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo["id"]}"} - %td - = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" - %td.import-target - = repo["path_with_namespace"] - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + - @repos.each do |repo| + %tr{id: "repo_#{repo["id"]}"} + %td + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" + %td.import-target + = repo["path_with_namespace"] + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" :coffeescript new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index bff7ee7c85..2e3a535737 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -9,38 +9,39 @@ %p = button_tag 'Import all projects', class: "btn btn-success js-import-all" -%table.table.import-jobs - %thead - %tr - %th From Gitorious.org - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From Gitorious.org + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank" - %td.import-target - = repo.full_name - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank" + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" :coffeescript new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index e8ec79e72f..c5af06edf8 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -17,45 +17,46 @@ - else = button_tag 'Import all projects', class: "btn btn-success js-import-all" -%table.table.import-jobs - %thead - %tr - %th From Google Code - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" - %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name +.table-holder + %table.table.import-jobs + %thead + %tr + %th From Google Code + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" - %td.import-target - = "#{current_user.username}/#{repo.name}" - %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" - - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" - %td.import-target - %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" - if @incompatible_repos.any? %p diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml index c19ac429d5..58af79716a 100644 --- a/app/views/profiles/_event_table.html.haml +++ b/app/views/profiles/_event_table.html.haml @@ -1,16 +1,17 @@ -%table.table#audits - %thead - %tr - %th Action - %th When - - %tbody - - events.each do |event| +.table-holder + %table.table#audits + %thead %tr - %td - %span - Signed in with - %b= event.details[:with] - authentication - %td #{time_ago_in_words event.created_at} ago + %th Action + %th When + + %tbody + - events.each do |event| + %tr + %td + %span + Signed in with + %b= event.details[:with] + authentication + %td #{time_ago_in_words event.created_at} ago = paginate events, theme: "gitlab" diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 4d8ca16d98..e08556673e 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -32,21 +32,22 @@ %li .nothing-here-block No builds to show - else - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Commit - %th Ref - %th Runner - %th Name - %th Duration - %th Finished at - %th + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Commit + %th Ref + %th Runner + %th Name + %th Duration + %th Finished at + %th - - @builds.each do |build| - = render 'projects/builds/build', build: build + - @builds.each do |build| + = render 'projects/builds/build', build: build = paginate @builds diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml index 6aebd7cfc4..369086b39e 100644 --- a/app/views/projects/ci_web_hooks/index.html.haml +++ b/app/views/projects/ci_web_hooks/index.html.haml @@ -20,17 +20,18 @@ -if @web_hooks.any? %h4 Activated web hooks (#{@web_hooks.count}) - %table.table - - @web_hooks.each do |hook| - %tr - %td - .clearfix - %span.monospace= hook.url - %td - .pull-right - - if @ci_project.commits.any? - = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + .table-holder + %table.table + - @web_hooks.each do |hook| + %tr + %td + .clearfix + %span.monospace= hook.url + %td + .pull-right + - if @ci_project.commits.any? + = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" %h4 Web Hook data example diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index ca71a91af1..43033cad24 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -29,27 +29,7 @@ - if @ci_commit.builds.running_or_pending.any? = link_to "Cancel all", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger' -%table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true } - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - +.table-holder %table.table.builds %thead %tr @@ -63,5 +43,27 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?) } + - @ci_commit.refs.each do |ref| + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, + locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true } + +- if @ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_project.try(:coverage_enabled?) } diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index bb49f4de87..f68449b186 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,34 +1,35 @@ - unless @branches.empty? %br %h4 Already Protected: - %table.table.protected-branches-list - %thead - %tr.no-border - %th Branch - %th Developers can push - %th Last commit - %th + .table-holder + %table.table.protected-branches-list + %thead + %tr.no-border + %th Branch + %th Developers can push + %th Last commit + %th - %tbody - - @branches.each do |branch| - - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) - %tr - %td - = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do - %strong= branch.name - - if @project.root_ref?(branch.name) - %span.label.label-info default + %tbody + - @branches.each do |branch| + - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) + %tr %td - = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url - %td - - if commit = branch.commit - = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do - = commit.short_id - · - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) - %td - .pull-right - - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do + %strong= branch.name + - if @project.root_ref?(branch.name) + %span.label.label-info default + %td + = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url + %td + - if commit = branch.commit + = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do + = commit.short_id + · + #{time_ago_with_tooltip(commit.committed_date)} + - else + (branch was removed from repository) + %td + .pull-right + - if can? current_user, :admin_project, @project + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index ffec495f85..c255cd51bd 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -9,56 +9,57 @@ %span.runner-state.runner-state-specific Specific -%table.table - %thead +.table-holder + %table.table + %thead + %tr + %th Property Name + %th Value %tr - %th Property Name - %th Value - %tr - %td - Tags - %td - - @runner.tag_list.each do |tag| - %span.label.label-primary - = tag - %tr - %td - Name - %td - = @runner.name - %tr - %td - Version - %td - = @runner.version - %tr - %td - Revision - %td - = @runner.revision - %tr - %td - Platform - %td - = @runner.platform - %tr - %td - Architecture - %td - = @runner.architecture - %tr - %td - Description - %td - = @runner.description - %tr - %td - Last contact - %td - - if @runner.contacted_at - #{time_ago_in_words(@runner.contacted_at)} ago - - else - Never + %td + Tags + %td + - @runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %tr + %td + Name + %td + = @runner.name + %tr + %td + Version + %td + = @runner.version + %tr + %td + Revision + %td + = @runner.revision + %tr + %td + Platform + %td + = @runner.platform + %tr + %td + Architecture + %td + = @runner.architecture + %tr + %td + Description + %td + = @runner.description + %tr + %td + Last contact + %td + - if @runner.contacted_at + #{time_ago_in_words(@runner.contacted_at)} ago + - else + Never - + diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index 1065def693..c1356f6db0 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -2,22 +2,23 @@ %h3.page-title Project services %p.light Project services allow you to integrate GitLab with other applications -%table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = boolean_to_icon service.activated? - %td - = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago +.table-holder + %table.table + %thead + %tr + %th + %th Service + %th Description + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 7ff48e32e6..b5510199ad 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -30,7 +30,7 @@ New directory %div#tree-content-holder.tree-content-holder - .tree-table-holder + .table-holder %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } %thead %tr diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 17dcb78e25..18a37302c3 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -7,12 +7,13 @@ %hr.clearfix -if @triggers.any? - %table.table - %thead - %th Token - %th Last used - %th - = render partial: 'trigger', collection: @triggers, as: :trigger + .table-holder + %table.table + %thead + %th Token + %th Last used + %th + = render partial: 'trigger', collection: @triggers, as: :trigger - else %h4 No triggers diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index bfbef823b3..4322146ce3 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -7,28 +7,29 @@ %span.light History for = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) -%table.table - %thead - %tr - %th Page version - %th Author - %th Commit Message - %th Last updated - %th Format - %tbody - - @page.versions.each_with_index do |version, index| - - commit = version +.table-holder + %table.table + %thead %tr - %td - = link_to project_wiki_path_with_version(@project, @page, - commit.id, index == 0) do - = truncate_sha(commit.id) - %td - = commit.author.name - %td - = commit.message - %td - #{time_ago_with_tooltip(version.authored_date)} - %td - %strong - = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + %th Page version + %th Author + %th Commit Message + %th Last updated + %th Format + %tbody + - @page.versions.each_with_index do |version, index| + - commit = version + %tr + %td + = link_to project_wiki_path_with_version(@project, @page, + commit.id, index == 0) do + = truncate_sha(commit.id) + %td + = commit.author.name + %td + = commit.message + %td + #{time_ago_with_tooltip(version.authored_date)} + %td + %strong + = @page.page.wiki.page(@page.page.name, commit.id).try(:format) From 6ad78d3ab1fc0ea9f344810e22b4fa7e8d67b6f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 19 Oct 2015 11:25:21 +0200 Subject: [PATCH 200/443] Move changelog item to 8.2 [ci skip] --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 46fec4bd92..08a5030f72 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL + - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) v 8.1.0 (unreleased) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) @@ -59,8 +60,6 @@ v 8.1.0 (unreleased) - Fix position of hamburger in header for smaller screens (Han Loong Liauw) - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) - Persist filters when sorting on admin user page (Jerry Lukins) - - Adds ability to remove the forked relationship from project settings - screen. (Han Loong Liauw) - Allow dashboard and group issues/MRs to be filtered by label - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed From 4ff75e317935f990b90dcc5869afe8ebb2b6fee6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 15 Oct 2015 18:10:35 +0200 Subject: [PATCH 201/443] Improve performance of sorting milestone issues This cuts down the time it takes to sort issues of a milestone by about 10x. In the previous setup the code would run a SQL query for every issue that had to be sorted. The new setup instead runs a single SQL query to update all the given issues at once. The attached benchmark used to run at around 60 iterations per second, using the new setup this hovers around 600 iterations per second. Timing wise a request to update a milestone with 40-something issues would take about 760 ms, in the new setup this only takes about 130 ms. Fixes #3066 --- CHANGELOG | 1 + .../projects/milestones_controller.rb | 6 +--- app/models/milestone.rb | 32 +++++++++++++++++++ spec/benchmarks/models/milestone_spec.rb | 17 ++++++++++ spec/models/milestone_spec.rb | 28 ++++++++++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 spec/benchmarks/models/milestone_spec.rb diff --git a/CHANGELOG b/CHANGELOG index f8daa6d246..7fefc90c54 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.2.0 (unreleased) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) + - Improved performance of sorting milestone issues v 8.1.0 (unreleased) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 86f4a02a6e..15506bd677 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -75,11 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController end def sort_issues - @issues = @milestone.issues.where(id: params['sortable_issue']) - @issues.each do |issue| - issue.position = params['sortable_issue'].index(issue.id.to_s) + 1 - issue.save - end + @milestone.sort_issues(params['sortable_issue'].map(&:to_i)) render json: { saved: true } end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 84acba30b6..2ff16e2825 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -105,4 +105,36 @@ class Milestone < ActiveRecord::Base def author_id nil end + + # Sorts the issues for the given IDs. + # + # This method runs a single SQL query using a CASE statement to update the + # position of all issues in the current milestone (scoped to the list of IDs). + # + # Given the ids [10, 20, 30] this method produces a SQL query something like + # the following: + # + # UPDATE issues + # SET position = CASE + # WHEN id = 10 THEN 1 + # WHEN id = 20 THEN 2 + # WHEN id = 30 THEN 3 + # ELSE position + # END + # WHERE id IN (10, 20, 30); + # + # This method expects that the IDs given in `ids` are already Fixnums. + def sort_issues(ids) + pairs = [] + + ids.each_with_index do |id, index| + pairs << id + pairs << index + 1 + end + + conditions = 'WHEN id = ? THEN ? ' * ids.length + + issues.where(id: ids). + update_all(["position = CASE #{conditions} ELSE position END", *pairs]) + end end diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb new file mode 100644 index 0000000000..a94afc4c40 --- /dev/null +++ b/spec/benchmarks/models/milestone_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Milestone, benchmark: true do + describe '#sort_issues' do + let(:milestone) { create(:milestone) } + + let(:issue1) { create(:issue, milestone: milestone) } + let(:issue2) { create(:issue, milestone: milestone) } + let(:issue3) { create(:issue, milestone: milestone) } + + let(:issue_ids) { [issue3.id, issue2.id, issue1.id] } + + benchmark_subject { milestone.sort_issues(issue_ids) } + + it { is_expected.to iterate_per_second(500) } + end +end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index c88d534966..77c5862732 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -140,4 +140,32 @@ describe Milestone do end end + describe '#sort_issues' do + let(:milestone) { create(:milestone) } + + let(:issue1) { create(:issue, milestone: milestone, position: 1) } + let(:issue2) { create(:issue, milestone: milestone, position: 2) } + let(:issue3) { create(:issue, milestone: milestone, position: 3) } + let(:issue4) { create(:issue, position: 42) } + + it 'sorts the given issues' do + milestone.sort_issues([issue3.id, issue2.id, issue1.id]) + + issue1.reload + issue2.reload + issue3.reload + + expect(issue1.position).to eq(3) + expect(issue2.position).to eq(2) + expect(issue3.position).to eq(1) + end + + it 'ignores issues not part of the milestone' do + milestone.sort_issues([issue3.id, issue2.id, issue1.id, issue4.id]) + + issue4.reload + + expect(issue4.position).to eq(42) + end + end end From 157d891615bbc9d1176b2149cfa5193b9aa4773c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 19 Oct 2015 11:40:13 +0200 Subject: [PATCH 202/443] Add changelog item --- CHANGELOG | 1 + app/controllers/projects_controller.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f8daa6d246..cfb7a581e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.2.0 (unreleased) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) + - Allow users to select the Files view as default project view (Cristian Bica) v 8.1.0 (unreleased) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 73200396ec..9f0cce468b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -246,6 +246,8 @@ class ProjectsController < ApplicationController project.repository_exists? && !project.empty_repo? end + # Override get_id from ExtractsPath, which returns the branch and file path + # for the blob/tree, which in this case is just the root of the default branch. def get_id project.repository.root_ref end From 8b8fbd4e7f51db32425dd30770c1efcea75c00f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 19 Oct 2015 11:46:22 +0200 Subject: [PATCH 203/443] Rename confusing methods --- app/finders/issuable_finder.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f00bb02d0f..c407dfc163 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -93,7 +93,7 @@ class IssuableFinder params[:milestone_title].present? end - def no_milestones? + def filter_by_no_milestone? milestones? && params[:milestone_title] == Milestone::None.title end @@ -114,7 +114,7 @@ class IssuableFinder params[:label_name].present? end - def no_labels? + def filter_by_no_label? labels? && params[:label_name] == Label::None.title end @@ -227,7 +227,7 @@ class IssuableFinder def by_milestone(items) if milestones? - if no_milestones? + if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) @@ -243,7 +243,7 @@ class IssuableFinder def by_label(items) if labels? - if no_labels? + if filter_by_no_label? items = items. joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). where(label_links: { id: nil }) From 4ad64ab3f4705b7fa88f855d67e0d2d268c5e395 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 19 Oct 2015 10:32:15 -0700 Subject: [PATCH 204/443] Fix duplicate repositories in GitHub import page By default, all the current user's repositories are accessible via the /users endpoint. There's no need to traverse all the organization repositories as well. See: * http://www.rubydoc.info/github/pengwynn/octokit/Octokit/Client/Repositories#repositories-instance_method * https://developer.github.com/v3/repos/#list-your-repositories Closes #2523 --- CHANGELOG | 1 + app/controllers/import/github_controller.rb | 4 ---- spec/controllers/import/github_controller_spec.rb | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 634cf15f94..5671d8b1d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Fix duplicate repositories in GitHub import page (Stan Hu) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index aae77d384c..67bf4190e7 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -11,10 +11,6 @@ class Import::GithubController < Import::BaseController def status @repos = client.repos - client.orgs.each do |org| - @repos += client.org_repos(org.login) - end - @already_added_projects = current_user.created_projects.where(import_type: "github") already_added_projects_names = @already_added_projects.pluck(:import_source) diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 766be578f7..bbf8adef53 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -41,7 +41,7 @@ describe Import::GithubController do it "assigns variables" do @project = create(:project, import_type: 'github', creator_id: user.id) - stub_client(repos: [@repo], orgs: [@org], org_repos: [@org_repo]) + stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo]) get :status From d02a467d64679dd8ff02b5f4554ae5700f0bdfd7 Mon Sep 17 00:00:00 2001 From: Benny Schimmer Date: Mon, 19 Oct 2015 22:14:22 +0200 Subject: [PATCH 205/443] Fix regex in redis version check --- lib/tasks/gitlab/check.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 606bf241db..2e73f792a9 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -335,7 +335,7 @@ namespace :gitlab do print "Redis version >= #{min_redis_version}? ... " redis_version = run(%W(redis-cli --version)) - redis_version = redis_version.try(:match, /redis-cli (.*)/) + redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) if redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) puts "yes".green From 8479feeba27800ee2e8286d759247339703f83be Mon Sep 17 00:00:00 2001 From: Benny Schimmer Date: Mon, 19 Oct 2015 23:10:17 +0200 Subject: [PATCH 206/443] Simplify patch version updates (less steps) --- doc/update/patch_versions.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index da719229ab..593722eb01 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -49,9 +49,7 @@ sudo -u git -H bundle install --without development test mysql --deployment sudo -u git -H bundle install --without development test postgres --deployment sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production -sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production -sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production -sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` ### 5. Start application From ffacb01be202f3b30d79116f84d0a2c7f37c96e2 Mon Sep 17 00:00:00 2001 From: Maurice Mohlek Date: Tue, 20 Oct 2015 09:40:21 +0200 Subject: [PATCH 207/443] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 634cf15f94..9f4dcf5fc5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,10 @@ v 8.2.0 (unreleased) - Improved performance of sorting milestone issues - Allow users to select the Files view as default project view (Cristian Bica) +v 8.0.5 + - Correct lookup-by-email for LDAP logins + - Fix loading spinner sometimes not being hidden on Merge Request tab switches + v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) From b0a3ece265350b8d02218746ff636a8363987580 Mon Sep 17 00:00:00 2001 From: Maurice Mohlek Date: Tue, 20 Oct 2015 09:44:13 +0200 Subject: [PATCH 208/443] Fix order --- CHANGELOG | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9f4dcf5fc5..d6b5e275fe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,10 +7,6 @@ v 8.2.0 (unreleased) - Improved performance of sorting milestone issues - Allow users to select the Files view as default project view (Cristian Bica) -v 8.0.5 - - Correct lookup-by-email for LDAP logins - - Fix loading spinner sometimes not being hidden on Merge Request tab switches - v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) @@ -83,6 +79,10 @@ v 8.1.0 (unreleased) - Optimize query when filtering on issuables (Zeger-Jan van de Weg) - Fix padding of outdated discussion item. +v 8.0.5 + - Correct lookup-by-email for LDAP logins + - Fix loading spinner sometimes not being hidden on Merge Request tab switches + v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) - Fix referrals for :back and relative URL installs From 8710739e4e5d12ac9e2aa88a553cc1e02dc2b2d1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 20 Oct 2015 14:23:56 +0200 Subject: [PATCH 209/443] Correctly find last known blob for file deleted in MR. --- app/controllers/projects/compare_controller.rb | 3 ++- .../projects/merge_requests_controller.rb | 5 ++++- app/helpers/diff_helper.rb | 3 ++- app/models/commit.rb | 12 ++++++++---- app/models/merge_request.rb | 14 +++++++++++++- app/models/merge_request_diff.rb | 4 ++++ app/models/repository.rb | 8 +------- app/views/projects/diffs/_diffs.html.haml | 4 ++-- app/views/projects/diffs/_file.html.haml | 4 ++-- 9 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d15004f93a..71aaad1fad 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -17,9 +17,10 @@ class Projects::CompareController < Projects::ApplicationController execute(@project, head_ref, @project, base_ref) if compare_result - @commits = compare_result.commits + @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs @commit = @commits.last + @first_commit = @commits.first @line_notes = [] end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0d9c546195..16c4238662 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -56,6 +56,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs @commit = @merge_request.last_commit + @first_commit = @merge_request.first_commit + @comments_allowed = @reply_allowed = true @comments_target = { noteable_type: 'MergeRequest', @@ -89,7 +91,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project @commits = @merge_request.compare_commits - @commit = @merge_request.compare_commits.last + @commit = @merge_request.last_commit + @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index b896fba370..e65e37211c 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -170,7 +170,8 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file - @merge_request ? @merge_request.commits.last : @commit.parents.first + first_commit = @first_commit || @commit + first_commit.parent else @commit end diff --git a/app/models/commit.rb b/app/models/commit.rb index 23b5e38336..492f6be1ce 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -164,6 +164,14 @@ class Commit @committer ||= User.find_by_any_email(committer_email) end + def parents + @parents ||= parent_ids.map { |id| project.commit(id) } + end + + def parent + @parent ||= project.commit(self.parent_id) if self.parent_id + end + def notes project.notes.for_commit_id(self.id) end @@ -181,10 +189,6 @@ class Commit @raw.short_id(7) end - def parents - @parents ||= Commit.decorate(super, project) - end - def ci_commit project.ci_commit(sha) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0042b95c4f..21861a46a8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -40,7 +40,7 @@ class MergeRequest < ActiveRecord::Base after_create :create_merge_request_diff after_update :update_merge_request_diff - delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil + delegate :commits, :diffs, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -157,6 +157,18 @@ class MergeRequest < ActiveRecord::Base reference end + def last_commit + merge_request_diff ? merge_request_diff.last_commit : compare_commits.last + end + + def first_commit + merge_request_diff ? merge_request_diff.first_commit : compare_commits.first + end + + def last_commit_short_sha + last_commit.short_id + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index bc2d691ece..6575d0bc81 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -55,6 +55,10 @@ class MergeRequestDiff < ActiveRecord::Base commits.first end + def first_commit + commits.last + end + def last_commit_short_sha @last_commit_short_sha ||= last_commit.short_id end diff --git a/app/models/repository.rb b/app/models/repository.rb index 921e1a9e42..e2d4f74407 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -312,13 +312,7 @@ class Repository end def blob_for_diff(commit, diff) - file = blob_at(commit.id, diff.new_path) - - unless file - file = prev_blob_for_diff(commit, diff) - end - - file + blob_at(commit.id, diff.file_path) end def prev_blob_for_diff(commit, diff) diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 4f1965bfb3..56b51f038b 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -15,8 +15,8 @@ .files - diff_files.each_with_index do |diff_file, index| - - diff_commit = commit_for_diff(diff_file.diff) - - blob = project.repository.blob_for_diff(diff_commit, diff_file.diff) + - diff_commit = commit_for_diff(diff_file) + - blob = project.repository.blob_for_diff(diff_commit, diff_file) - next unless blob = render 'projects/diffs/file', i: index, project: project, diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 9698921f6d..410ff6abb4 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,5 +1,5 @@ .diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)} - .diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"} + .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) @@ -38,7 +38,7 @@ - else = render "projects/diffs/text_file", diff_file: diff_file, index: i - elsif blob.image? - - old_file = project.repository.prev_blob_for_diff(@commit, diff_file) + - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i - else .nothing-here-block No preview for this file type From afdc028516f27651d4d94ffd568765cf640c0c44 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 20 Oct 2015 15:49:11 +0200 Subject: [PATCH 210/443] Speed up searching for text references a bit If a node is ignored there's no need for searching for a given pattern. In turn, when searching for the pattern there's no need to construct a MatchData object as we only care about presence (or lack thereof), not the resulting matches. In terms of performance this cuts down about 200 ms when loading issue #2164 locally, though this varies a bit depending on system load. --- CHANGELOG | 1 + lib/gitlab/markdown/reference_filter.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5671d8b1d8..3044ebbd7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Improved performance of replacing references in comments - Fix duplicate repositories in GitHub import page (Stan Hu) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index adaca78ba2..a4c560f578 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -15,7 +15,7 @@ module Gitlab LazyReference = Struct.new(:klass, :ids) do def self.load(refs) lazy_references, values = refs.partition { |ref| ref.is_a?(self) } - + lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs| ids = refs.flat_map(&:ids) klass.where(id: ids) @@ -107,10 +107,10 @@ module Gitlab return doc if project.nil? search_text_nodes(doc).each do |node| - content = node.to_html - - next unless content.match(pattern) next if ignored_ancestry?(node) + next unless node.text =~ pattern + + content = node.to_html html = yield content From e1c3077e4bb718ce841fad175f708623d8375818 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 20 Oct 2015 15:51:02 +0200 Subject: [PATCH 211/443] Added benchmark for ReferenceFilter --- .../gitlab/markdown/reference_filter_spec.rb | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb new file mode 100644 index 0000000000..34cd9f7e4e --- /dev/null +++ b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Markdown::ReferenceFilter, benchmark: true do + let(:input) do + html = <<-EOF +

Hello @alice and @bob, how are you doing today?

+

This is simple @dummy text to see how the @ReferenceFilter class performs +when @processing HTML.

+ EOF + + Nokogiri::HTML.fragment(html) + end + + let(:project) { create(:empty_project) } + + let(:filter) { described_class.new(input, project: project) } + + describe '#replace_text_nodes_matching' do + let(:iterations) { 6000 } + + describe 'with identical input and output HTML' do + benchmark_subject do + filter.replace_text_nodes_matching(User.reference_pattern) do |content| + content + end + end + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'with different input and output HTML' do + benchmark_subject do + filter.replace_text_nodes_matching(User.reference_pattern) do |content| + '@eve' + end + end + + it { is_expected.to iterate_per_second(iterations) } + end + end +end From 2f7fc7e9f7e7a43914abe81a510bd0dffa113979 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 20 Oct 2015 16:16:08 +0200 Subject: [PATCH 212/443] Prefer project with exact path to differently cased one when both exist. --- app/controllers/application_controller.rb | 4 +-- app/models/project.rb | 17 +++++++---- spec/controllers/projects_controller_spec.rb | 30 ++++++++++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f0124c6bd6..38e6b44eb6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -118,8 +118,8 @@ class ApplicationController < ActionController::Base end project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) - + @project = Project.find_with_namespace(project_path) || + Project.find_with_namespace(project_path, case_sensitive: false) if @project and can?(current_user, :read_project, @project) if @project.path_with_namespace != project_path diff --git a/app/models/project.rb b/app/models/project.rb index 88cd88dcb5..b2a8dde9ba 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -235,7 +235,7 @@ class Project < ActiveRecord::Base where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") end - def find_with_namespace(id) + def find_with_namespace(id, case_sensitive: true) namespace_path, project_path = id.split('/') return nil if !namespace_path || !project_path @@ -243,11 +243,18 @@ class Project < ActiveRecord::Base # Use of unscoped ensures we're not secretly adding any ORDER BYs, which # have a negative impact on performance (and aren't needed for this # query). - unscoped. + projects = unscoped. joins(:namespace). - iwhere('namespaces.path' => namespace_path). - iwhere('projects.path' => project_path). - take + iwhere('namespaces.path' => namespace_path) + + projects = + if case_sensitive + projects.where('projects.path' => project_path) + else + projects.iwhere('projects.path' => project_path) + end + + projects.take end def visibility_levels diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4460bf12f9..5090f87c73 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -51,16 +51,34 @@ describe ProjectsController do end context "when requested with case sensitive namespace and project path" do - it "redirects to the normalized path for case mismatch" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + context "when there is a match with the same casing" do + it "loads the project" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path - expect(response).to redirect_to("/#{public_project.path_with_namespace}") + expect(assigns(:project)).to eq(public_project) + expect(response.status).to eq(200) + end end - it "loads the page if normalized path matches request path" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path + context "when there is a match with different casing" do + it "redirects to the normalized path" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase - expect(response.status).to eq(200) + expect(assigns(:project)).to eq(public_project) + expect(response).to redirect_to("/#{public_project.path_with_namespace}") + end + + context "when there is also a match with the same casing" do + + let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) } + + it "loads the exactly matched project" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + + expect(assigns(:project)).to eq(other_project) + expect(response.status).to eq(200) + end + end end end end From 9bfc531ec611d108c45af239a1e5e016b892231b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 20 Oct 2015 00:28:28 -0700 Subject: [PATCH 213/443] Redirect to a default path if HTTP_REFERER is not set Safari 9.0 does not yet honor the HTML5 `origin-when-cross-origin` mode, and it's possible load balancers/proxies strip the HTTP_REFERER from the request header. In these cases, default to some default path. Closes #3122 Closes https://github.com/gitlabhq/gitlabhq/issues/9731 --- CHANGELOG | 1 + .../admin/broadcast_messages_controller.rb | 2 +- app/controllers/admin/hooks_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 26 ++++++---- app/controllers/application_controller.rb | 4 ++ .../import/google_code_controller.rb | 6 +-- app/controllers/invites_controller.rb | 4 +- .../profiles/notifications_controller.rb | 2 +- app/controllers/profiles_controller.rb | 2 +- .../projects/ci_services_controller.rb | 2 +- .../projects/ci_web_hooks_controller.rb | 2 +- .../projects/deploy_keys_controller.rb | 2 +- app/controllers/projects/hooks_controller.rb | 2 +- app/controllers/projects/issues_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 4 +- .../projects/project_members_controller.rb | 3 +- .../projects/services_controller.rb | 4 +- .../admin/users_controller_spec.rb | 26 ++++++++++ spec/controllers/invites_controller_spec.rb | 33 +++++++++++++ .../projects/services_controller_spec.rb | 47 +++++++++++++------ 20 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 spec/controllers/invites_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 5671d8b1d8..8ea1161a4c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Fix duplicate repositories in GitHub import page (Stan Hu) + - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 0808024fc3..497c34f8f4 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -19,7 +19,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController BroadcastMessage.find(params[:id]).destroy respond_to do |format| - format.html { redirect_to :back } + format.html { redirect_back_or_default(default: { action: 'index' }) } format.js { render nothing: true } end end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index d670386f8c..0bd19c49d8 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -35,7 +35,7 @@ class Admin::HooksController < Admin::ApplicationController } @hook.execute(data, 'system_hooks') - redirect_to :back + redirect_back_or_default end def hook_params diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 00f41a10dd..c63d0793e3 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -33,33 +33,33 @@ class Admin::UsersController < Admin::ApplicationController def block if user.block - redirect_to :back, notice: "Successfully blocked" + redirect_back_or_admin_user(notice: "Successfully blocked") else - redirect_to :back, alert: "Error occurred. User was not blocked" + redirect_back_or_admin_user(alert: "Error occurred. User was not blocked") end end def unblock if user.activate - redirect_to :back, notice: "Successfully unblocked" + redirect_back_or_admin_user(notice: "Successfully unblocked") else - redirect_to :back, alert: "Error occurred. User was not unblocked" + redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") end end def unlock if user.unlock_access! - redirect_to :back, alert: "Successfully unlocked" + redirect_back_or_admin_user(alert: "Successfully unlocked") else - redirect_to :back, alert: "Error occurred. User was not unlocked" + redirect_back_or_admin_user(alert: "Error occurred. User was not unlocked") end end def confirm if user.confirm - redirect_to :back, notice: "Successfully confirmed" + redirect_back_or_admin_user(notice: "Successfully confirmed") else - redirect_to :back, alert: "Error occurred. User was not confirmed" + redirect_back_or_admin_user(alert: "Error occurred. User was not confirmed") end end @@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController user.update_secondary_emails! respond_to do |format| - format.html { redirect_to :back, notice: "Successfully removed email." } + format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } format.js { render nothing: true } end end @@ -157,4 +157,12 @@ class Admin::UsersController < Admin::ApplicationController :projects_limit, :can_create_group, :admin, :key_id ) end + + def redirect_back_or_admin_user(options = {}) + redirect_back_or_default(default: default_route, options: options) + end + + def default_route + [:admin, @user] + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f0124c6bd6..865deb7d46 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -33,6 +33,10 @@ class ApplicationController < ActionController::Base render_404 end + def redirect_back_or_default(default: root_path, options: {}) + redirect_to request.referer.present? ? :back : default, options + end + protected # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 41472a6fe6..e0de31f225 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -10,18 +10,18 @@ class Import::GoogleCodeController < Import::BaseController dump_file = params[:dump_file] unless dump_file.respond_to?(:read) - return redirect_to :back, alert: "You need to upload a Google Takeout archive." + return redirect_back_or_default(options: { alert: "You need to upload a Google Takeout archive." }) end begin dump = JSON.parse(dump_file.read) rescue - return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." }) end client = Gitlab::GoogleCodeImport::Client.new(dump) unless client.valid? - return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." }) end session[:google_code_dump] = dump diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 8ef10a17f5..94bb108c5f 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -14,7 +14,7 @@ class InvitesController < ApplicationController redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}." else - redirect_to :back, alert: "The invitation could not be accepted." + redirect_back_or_default(options: { alert: "The invitation could not be accepted." }) end end @@ -31,7 +31,7 @@ class InvitesController < ApplicationController redirect_to path, notice: "You have declined the invitation to join #{label}." else - redirect_to :back, alert: "The invitation could not be declined." + redirect_back_or_default(options: { alert: "The invitation could not be declined." }) end end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 22423651c1..1fd1d6882d 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -29,7 +29,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController flash[:alert] = "Failed to save new settings" end - redirect_to :back + redirect_back_or_default(default: profile_notifications_path) end format.js diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 26a4de1546..8da7b4d50e 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -26,7 +26,7 @@ class ProfilesController < Profiles::ApplicationController end respond_to do |format| - format.html { redirect_to :back } + format.html { redirect_back_or_default(default: { action: 'show' }) } end end diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb index 6d2756eba3..406f313ae7 100644 --- a/app/controllers/projects/ci_services_controller.rb +++ b/app/controllers/projects/ci_services_controller.rb @@ -30,7 +30,7 @@ class Projects::CiServicesController < Projects::ApplicationController message = { alert: 'We tried to test the service but error occurred' } end - redirect_to :back, message + redirect_back_or_default(options: message) end private diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb index 7f40ddcb3f..a2d470d4a6 100644 --- a/app/controllers/projects/ci_web_hooks_controller.rb +++ b/app/controllers/projects/ci_web_hooks_controller.rb @@ -24,7 +24,7 @@ class Projects::CiWebHooksController < Projects::ApplicationController def test Ci::TestHookService.new.execute(hook, current_user) - redirect_to :back + redirect_back_or_default(default: { action: 'index' }) end def destroy diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 40e2b37912..7d09288bc8 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -46,7 +46,7 @@ class Projects::DeployKeysController < Projects::ApplicationController def disable @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy - redirect_to :back + redirect_back_or_default(default: { action: 'index' }) end protected diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 4e5b4125f5..c756954189 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -37,7 +37,7 @@ class Projects::HooksController < Projects::ApplicationController flash[:alert] = 'Hook execution failed. Ensure the project has commits.' end - redirect_to :back + redirect_back_or_default(default: { action: 'index' }) end def destroy diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cc8321d97a..e767efbdc0 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -106,7 +106,7 @@ class Projects::IssuesController < Projects::ApplicationController def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute - redirect_to :back, notice: "#{result[:count]} issues updated" + redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end def toggle_subscription diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 0f5d82ce13..41cd08c93c 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -25,7 +25,7 @@ class Projects::NotesController < Projects::ApplicationController respond_to do |format| format.json { render_note_json(@note) } - format.html { redirect_to :back } + format.html { redirect_back_or_default } end end @@ -34,7 +34,7 @@ class Projects::NotesController < Projects::ApplicationController respond_to do |format| format.json { render_note_json(@note) } - format.html { redirect_to :back } + format.html { redirect_back_or_default } end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cf73bc01c8..9de5269cd2 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -72,7 +72,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController def leave if @project.namespace == current_user.namespace - return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.') + message = 'You can not leave your own project. Transfer or delete the project.' + return redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) end @project.project_members.find_by(user_id: current_user).destroy diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 129068ef01..42dbb497e0 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -12,7 +12,7 @@ class Projects::ServicesController < Projects::ApplicationController # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] - + # Authorize before_action :authorize_admin_project! before_action :service, only: [:edit, :update, :test] @@ -52,7 +52,7 @@ class Projects::ServicesController < Projects::ApplicationController message = { alert: error_message } end - redirect_to :back, message + redirect_back_or_default(options: message) end private diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 7168db117d..fcbe62cace 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -37,6 +37,32 @@ describe Admin::UsersController do end end + describe 'PUT block/:id' do + let(:user) { create(:user) } + + it 'blocks user' do + put :block, id: user.username + user.reload + expect(user.blocked?).to be_truthy + expect(flash[:notice]).to eq 'Successfully blocked' + end + end + + describe 'PUT unblock/:id' do + let(:user) { create(:user) } + + before do + user.block + end + + it 'unblocks user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_falsey + expect(flash[:notice]).to eq 'Successfully unblocked' + end + end + describe 'PUT unlock/:id' do let(:user) { create(:user) } diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb new file mode 100644 index 0000000000..3c6e54839b --- /dev/null +++ b/spec/controllers/invites_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe InvitesController do + let(:token) { '123456' } + let(:user) { create(:user) } + let(:member) { create(:project_member, invite_token: token, invite_email: 'test@abc.com', user: user) } + + before do + controller.instance_variable_set(:@member, member) + sign_in(user) + end + + describe 'GET #accept' do + it 'accepts user' do + get :accept, id: token + member.reload + + expect(response.status).to eq(302) + expect(member.user).to eq(user) + expect(flash[:notice]).to include 'You have been granted' + end + end + + describe 'GET #decline' do + it 'declines user' do + get :decline, id: token + expect{member.reload}.to raise_error ActiveRecord::RecordNotFound + + expect(response.status).to eq(302) + expect(flash[:notice]).to include 'You have declined the invitation to join' + end + end +end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index d4ecd98e12..ccd8c741c8 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -10,26 +10,43 @@ describe Projects::ServicesController do project.team << [user, :master] controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@service, service) - request.env["HTTP_REFERER"] = "/" end - describe "#test" do - context 'success' do - it "should redirect and show success message" do - expect(service).to receive(:test).and_return({ success: true, result: 'done' }) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html - expect(response.status).to redirect_to('/') - expect(flash[:notice]).to eq('We sent a request to the provided URL') - end + shared_examples_for 'services controller' do |referrer| + before do + request.env["HTTP_REFERER"] = referrer end - context 'failure' do - it "should redirect and show failure message" do - expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) - get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html - expect(response.status).to redirect_to('/') - expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') + describe "#test" do + context 'success' do + it "should redirect and show success message" do + expect(service).to receive(:test).and_return({ success: true, result: 'done' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:notice]).to eq('We sent a request to the provided URL') + end end + + context 'failure' do + it "should redirect and show failure message" do + expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') + end + end + end + end + + describe 'referrer defined' do + it_should_behave_like 'services controller' do + let!(:referrer) { "/" } + end + end + + describe 'referrer undefined' do + it_should_behave_like 'services controller' do + let!(:referrer) { nil } end end end From e1fff018d754df432accb1b211c44dd046b065e1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 16 Oct 2015 18:39:17 +0200 Subject: [PATCH 214/443] Clear archive cache asynchronously --- CHANGELOG | 1 + app/models/repository.rb | 16 ++++++++-------- app/services/archive_repository_service.rb | 2 +- app/workers/repository_archive_cache_worker.rb | 9 +++++++++ spec/services/archive_repository_service_spec.rb | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 app/workers/repository_archive_cache_worker.rb diff --git a/CHANGELOG b/CHANGELOG index 57768d6b3e..0f9711adae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -67,6 +67,7 @@ v 8.1.0 (unreleased) - Only render 404 page from /public - Hide passwords from services API (Alex Lossent) - Fix: Images cannot show when projects' path was changed + - Let gitlab-git-http-server generate and serve 'git archive' downloads v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index 921e1a9e42..d55d695b42 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -8,6 +8,14 @@ class Repository attr_accessor :raw_repository, :path_with_namespace, :project + def self.clean_old_archives + repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + + return unless File.directory?(repository_downloads_path) + + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) + end + def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace @project = project @@ -269,14 +277,6 @@ class Repository end # Remove archives older than 2 hours - def clean_old_archives - repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path - - return unless File.directory?(repository_downloads_path) - - Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) - end - def branches_sorted_by(value) case value when 'recently_updated' diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb index 6414b5a018..2160bf13e6 100644 --- a/app/services/archive_repository_service.rb +++ b/app/services/archive_repository_service.rb @@ -7,7 +7,7 @@ class ArchiveRepositoryService end def execute(options = {}) - project.repository.clean_old_archives + RepositoryArchiveCacheWorker.perform_async metadata = project.repository.archive_metadata(ref, storage_path, format) raise "Repository or ref not found" if metadata.empty? diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb new file mode 100644 index 0000000000..47c5a670ed --- /dev/null +++ b/app/workers/repository_archive_cache_worker.rb @@ -0,0 +1,9 @@ +class RepositoryArchiveCacheWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + Repository.clean_old_archives + end +end diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb index 1cc7b24021..f7a36cd967 100644 --- a/spec/services/archive_repository_service_spec.rb +++ b/spec/services/archive_repository_service_spec.rb @@ -6,7 +6,7 @@ describe ArchiveRepositoryService do describe "#execute" do it "cleans old archives" do - expect(project.repository).to receive(:clean_old_archives) + expect(RepositoryArchiveCacheWorker).to receive(:perform_async) subject.execute(timeout: 0.0) end From a321404fde85fba6b09cbf1a56914f3a2ae555e4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 20 Oct 2015 17:11:26 +0200 Subject: [PATCH 215/443] Remove unused sidekiq worker --- app/workers/repository_archive_worker.rb | 43 ------------------------ 1 file changed, 43 deletions(-) delete mode 100644 app/workers/repository_archive_worker.rb diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb deleted file mode 100644 index 021c113956..0000000000 --- a/app/workers/repository_archive_worker.rb +++ /dev/null @@ -1,43 +0,0 @@ -class RepositoryArchiveWorker - include Sidekiq::Worker - - sidekiq_options queue: :archive_repo - - attr_accessor :project, :ref, :format - - def perform(project_id, ref, format) - @project = Project.find(project_id) - @ref, @format = ref, format.downcase - - repository = project.repository - - repository.clean_old_archives - - return unless file_path - return if archived? || archiving? - - repository.archive_repo(ref, storage_path, format) - end - - private - - def storage_path - Gitlab.config.gitlab.repository_downloads_path - end - - def file_path - @file_path ||= project.repository.archive_file_path(ref, storage_path, format) - end - - def pid_file_path - @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) - end - - def archived? - File.exist?(file_path) - end - - def archiving? - File.exist?(pid_file_path) - end -end From 8052c4ef4e23ea4f4a221fbc6ead138f33fb0f91 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 20 Oct 2015 16:09:11 +0000 Subject: [PATCH 216/443] Merge branch 'rs-8-1-update-guide-changes' into 'master' Update 8.0-to-8.1 update guide - User needs to update gitlab-git-http-server - User needs to update Nginx configuration [ci skip] See merge request !1625 --- doc/update/8.0-to-8.1.md | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md index 4dacc97f7f..d57c0d0674 100644 --- a/doc/update/8.0-to-8.1.md +++ b/doc/update/8.0-to-8.1.md @@ -70,7 +70,16 @@ sudo -u git -H git fetch sudo -u git -H git checkout v2.6.5 ``` -### 5. Install libs, migrations, etc. +### 5. Update gitlab-git-http-server + +```bash +cd /home/git/gitlab-git-http-server +sudo -u git -H git fetch origin +sudo -u git -H git checkout 0.3.0 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -91,7 +100,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab ``` -### 6. Update configuration files +### 7. Update configuration files #### New configuration options for `gitlab.yml` @@ -101,12 +110,33 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example ``` -### 7. Start application +#### Nginx configuration + +View changes between the previous recommended Nginx configuration and the +current one: + +```sh +# For HTTPS configurations +git diff origin/8-0-stable:lib/support/nginx/gitlab-ssl origin/8-1-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-0-stable:lib/support/nginx/gitlab origin/8-1-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-git-http-server listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-1-stable/lib/support/init.d/gitlab.default.example#L34 + +### 8. Start application sudo service gitlab start sudo service nginx restart -### 8. Check application status +### 9. Check application status Check if GitLab and its environment are configured correctly: From 8f4ae40e3c4a93114811637c0a643ae1b7ac86bf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Oct 2015 13:37:20 +0200 Subject: [PATCH 217/443] Add missing migrations --- db/migrate/20151019111551_fix_build_tags.rb | 5 +++++ db/migrate/20151019111703_fail_build_without_names.rb | 5 +++++ db/schema.rb | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151019111551_fix_build_tags.rb create mode 100644 db/migrate/20151019111703_fail_build_without_names.rb diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb new file mode 100644 index 0000000000..84b142183f --- /dev/null +++ b/db/migrate/20151019111551_fix_build_tags.rb @@ -0,0 +1,5 @@ +class FixBuildTags < ActiveRecord::Migration + def change + execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'") + end +end diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb new file mode 100644 index 0000000000..546b03d812 --- /dev/null +++ b/db/migrate/20151019111703_fail_build_without_names.rb @@ -0,0 +1,5 @@ +class FailBuildWithoutNames < ActiveRecord::Migration + def change + execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'") + end +end diff --git a/db/schema.rb b/db/schema.rb index b05fa70877..93afea35eb 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: 20151016195706) do +ActiveRecord::Schema.define(version: 20151019111703) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From f9f6724f552b15982f7c8cd5c3cab5b4b2380468 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Oct 2015 19:41:45 +0200 Subject: [PATCH 218/443] Added extra index for faster enumeration of CI builds --- db/migrate/20151020173906_add_ci_builds_index_for_status.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151020173906_add_ci_builds_index_for_status.rb diff --git a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb new file mode 100644 index 0000000000..c3f0e0606d --- /dev/null +++ b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb @@ -0,0 +1,5 @@ +class AddCiBuildsIndexForStatus < ActiveRecord::Migration + def change + add_index :ci_builds, [:commit_id, :status, :type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 93afea35eb..0fec00ebf8 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: 20151019111703) do +ActiveRecord::Schema.define(version: 20151020173906) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -110,6 +110,7 @@ ActiveRecord::Schema.define(version: 20151019111703) do end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree + add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree From 0f2b10c69fc82ffc2248a94b3dbd9eab06cff5b5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 08:41:53 +0200 Subject: [PATCH 219/443] Fix duplication between tree_header and tree_content --- .../projects/tree/_tree_content.html.haml | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index a5e478a38f..ee4c9d1693 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,34 +1,3 @@ -.gray-content-block - %ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - tree_breadcrumbs(tree, 6) do |title, path| - %li - - if path - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - - else - = link_to title, '#' - - if allowed_tree_edit? - %li - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} - = icon('plus') - %ul.dropdown-menu - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do - = icon('pencil fw') - Create file - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do - = icon('file fw') - Upload file - %li.divider - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do - = icon('folder fw') - New directory - %div.tree-content-holder .table-holder %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } From dd0552c36d0e043ae202c2fbb2927ffe99a6de79 Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Fri, 9 Oct 2015 13:45:37 +0800 Subject: [PATCH 220/443] Animate the logo on hover The logo is now rendered as pure SVG, rather than image referencing a svg. The SVG has an id and the shapes of the logo have a shared class. The shapes change their fill color on hover with a transition. --- CHANGELOG | 3 ++- app/assets/images/logo.svg | 16 +++++++------- app/assets/stylesheets/framework/sidebar.scss | 14 ++++++++++--- app/helpers/appearances_helper.rb | 2 +- app/views/shared/_logo.svg | 21 +++++++++++++++++++ 5 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 app/views/shared/_logo.svg diff --git a/CHANGELOG b/CHANGELOG index e956d074d7..f407fcbe18 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,7 +62,7 @@ v 8.1.0 (unreleased) - Move CI web hooks page to project settings area - Fix User Identities API. It now allows you to properly create or update user's identities. - Add user preference to change layout width (Peter Göbel) - - Use commit status in merge request widget as preffered source of CI status + - Use commit status in merge request widget as preferred source of CI status - Integrate CI commit and build pages into project pages - Move CI services page to project settings area - Add "Quick Submit" behavior to input fields throughout the application. Use @@ -85,6 +85,7 @@ v 8.1.0 (unreleased) v 8.0.5 - Correct lookup-by-email for LDAP logins - Fix loading spinner sometimes not being hidden on Merge Request tab switches + - Animate the logo on hover v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg index c09785cb96..f4e19b6700 100644 --- a/app/assets/images/logo.svg +++ b/app/assets/images/logo.svg @@ -10,17 +10,17 @@ - - - - - - - + + + + + + + - \ No newline at end of file + diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c5ea3aca7c..cd5234a1e0 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -239,9 +239,7 @@ padding: 10px 22px; overflow: hidden; - img { - width: 36px; - height: 36px; + #tanuki-logo { float: left; } @@ -265,3 +263,13 @@ } } } + + +.tanuki-shape { + transition: all 0.8s; + + &:hover { + fill: rgb(255, 255, 255); + transition: all 0.1s; + } +} diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 14df8d4cbd..c5820bf4c5 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -16,6 +16,6 @@ module AppearancesHelper end def brand_header_logo - image_tag 'logo.svg' + render 'shared/logo.svg' end end diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg new file mode 100644 index 0000000000..da49c48acd --- /dev/null +++ b/app/views/shared/_logo.svg @@ -0,0 +1,21 @@ + From 2a28b21054d76e736e9fd072a00d64924600a17a Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Wed, 21 Oct 2015 08:58:14 +0200 Subject: [PATCH 221/443] the logo svg should not mess with replacement imgs --- app/assets/stylesheets/framework/sidebar.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index cd5234a1e0..985ea16457 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -239,7 +239,12 @@ padding: 10px 22px; overflow: hidden; - #tanuki-logo { + img { + width: 36px; + height: 36px; + } + + #tanuki-logo, img { float: left; } From e39fdc1ddd85857d96c0da2e420e66bc67bd7c8c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 20 Oct 2015 11:23:33 -0700 Subject: [PATCH 222/443] Ensure MySQL CI limits DB migrations occur after the fields have been created Closes https://github.com/gitlabhq/gitlabhq/issues/9753 --- CHANGELOG | 1 + db/migrate/20151020173516_ci_limits_to_mysql.rb | 9 +++++++++ db/migrate/limits_to_mysql.rb | 4 ---- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20151020173516_ci_limits_to_mysql.rb diff --git a/CHANGELOG b/CHANGELOG index e956d074d7..dfc849fe7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Improved performance of replacing references in comments - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) diff --git a/db/migrate/20151020173516_ci_limits_to_mysql.rb b/db/migrate/20151020173516_ci_limits_to_mysql.rb new file mode 100644 index 0000000000..9bb960082f --- /dev/null +++ b/db/migrate/20151020173516_ci_limits_to_mysql.rb @@ -0,0 +1,9 @@ +class CiLimitsToMysql < ActiveRecord::Migration + def change + return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ + + # CI + change_column :ci_builds, :trace, :text, limit: 1073741823 + change_column :ci_commits, :push_data, :text, limit: 16777215 + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 73605d4c5e..2b7afae6d7 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -6,9 +6,5 @@ class LimitsToMysql < ActiveRecord::Migration change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 - - # CI - change_column :ci_builds, :trace, :text, limit: 1073741823 - change_column :ci_commits, :push_data, :text, limit: 16777215 end end From e17e5a5ce462022761d3cdc29d677f968ca9738a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 10:09:32 +0200 Subject: [PATCH 223/443] Move case sensitivity check to find_with_namespace. --- app/controllers/application_controller.rb | 3 +-- app/models/project.rb | 12 +++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 38e6b44eb6..1a47a3c0ee 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -118,8 +118,7 @@ class ApplicationController < ActionController::Base end project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) || - Project.find_with_namespace(project_path, case_sensitive: false) + @project = Project.find_with_namespace(project_path) if @project and can?(current_user, :read_project, @project) if @project.path_with_namespace != project_path diff --git a/app/models/project.rb b/app/models/project.rb index b2a8dde9ba..f9bed7cf89 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -235,7 +235,7 @@ class Project < ActiveRecord::Base where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") end - def find_with_namespace(id, case_sensitive: true) + def find_with_namespace(id) namespace_path, project_path = id.split('/') return nil if !namespace_path || !project_path @@ -247,14 +247,8 @@ class Project < ActiveRecord::Base joins(:namespace). iwhere('namespaces.path' => namespace_path) - projects = - if case_sensitive - projects.where('projects.path' => project_path) - else - projects.iwhere('projects.path' => project_path) - end - - projects.take + projects.where('projects.path' => project_path).take || + projects.iwhere('projects.path' => project_path).take end def visibility_levels From 4a02dfa48f46688acdf03d2901cb4f36364dccdf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 10:09:40 +0200 Subject: [PATCH 224/443] Disable case sensitive spec for MySQL. --- spec/controllers/projects_controller_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 5090f87c73..0fe9d4da84 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -73,6 +73,9 @@ describe ProjectsController do let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) } it "loads the exactly matched project" do + # MySQL queries are case insensitive by default, so this spec would fail. + skip if Gitlab::Database.mysql? + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase expect(assigns(:project)).to eq(other_project) From ab1b4936f4f9f4119e848e5b653bf38df466b7d4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 10:15:13 +0200 Subject: [PATCH 225/443] Fix margin around Markdown Write/Preview tabs. --- app/assets/stylesheets/framework/typography.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index e6d1cca9f7..e6558a2385 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -238,10 +238,6 @@ a > code { @include md-typography; } -.md-area { - @include md-typography; -} - .md { @include md-typography; } @@ -252,6 +248,7 @@ a > code { */ textarea.js-gfm-input { font-family: $monospace_font; + color: $gl-text-color; } .md-preview { From 310f49af35dad496041b21bbb8b05c74c14075ba Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 10:21:01 +0200 Subject: [PATCH 226/443] Don't load entire spec with MySQL --- spec/controllers/projects_controller_spec.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 0fe9d4da84..9b49f60b66 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -68,18 +68,20 @@ describe ProjectsController do expect(response).to redirect_to("/#{public_project.path_with_namespace}") end - context "when there is also a match with the same casing" do - let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) } + # MySQL queries are case insensitive by default, so this spec would fail. + unless Gitlab::Database.mysql? + context "when there is also a match with the same casing" do - it "loads the exactly matched project" do - # MySQL queries are case insensitive by default, so this spec would fail. - skip if Gitlab::Database.mysql? + let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) } - get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + it "loads the exactly matched project" do - expect(assigns(:project)).to eq(other_project) - expect(response.status).to eq(200) + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + + expect(assigns(:project)).to eq(other_project) + expect(response.status).to eq(200) + end end end end From de9886bece6455f2a35ce5128cb5444fdb5c4659 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 11:10:09 +0200 Subject: [PATCH 227/443] Restyle dashboard snippets visibility level tabs. --- app/assets/stylesheets/framework/buttons.scss | 11 ++++++++++ app/assets/stylesheets/framework/mixins.scss | 1 - app/helpers/tab_helper.rb | 18 ----------------- app/views/dashboard/snippets/index.html.haml | 20 ++++++++----------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e5f0c0ad9e..0402441958 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -162,10 +162,21 @@ border-color: #e7e9ed; width: 140px; + .badge { + font-weight: normal; + background-color: #eee; + color: #78a; + } + &.active { border-color: $gl-info; background: $gl-info; color: #fff; + + .badge { + color: $gl-info; + background-color: white; + } } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 089e6958ee..1d79834489 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -147,7 +147,6 @@ .badge { font-weight: normal; - background-color: #fff; background-color: #eee; color: #78a; } diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 0e7d8065ac..04e53fe7c6 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -110,22 +110,4 @@ module TabHelper 'active' end end - - # Use nav_tab for save controller/action but different params - def nav_tab(key, value, &block) - o = {} - o[:class] = "" - - if value.nil? - o[:class] << " active" if params[key].blank? - else - o[:class] << " active" if params[key] == value - end - - if block_given? - content_tag(:li, capture(&block), o) - else - content_tag(:li, nil, o) - end - end end diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 7ac3a12baf..2b67fd05b9 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -9,27 +9,23 @@ = icon('plus') New Snippet - .oneline - Share code pastes with others out of git repository - -%ul.center-top-menu.no-top.no-bottom.snippet-scope-menu - = nav_tab :scope, nil do - = link_to dashboard_snippets_path do + .btn-group.btn-group-next.event-filter + = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do All %span.badge = current_user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to dashboard_snippets_path(scope: 'are_private') do + + = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do Private %span.badge = current_user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to dashboard_snippets_path(scope: 'are_internal') do + + = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do Internal %span.badge = current_user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to dashboard_snippets_path(scope: 'are_public') do + + = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do Public %span.badge = current_user.snippets.are_public.count From b7b0010f7b9fa70d13d499bf1c4b8c18a695989e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Oct 2015 19:43:18 +0200 Subject: [PATCH 228/443] Remove CI migration task --- CHANGELOG | 1 + lib/ci/migrate/builds.rb | 29 ------------- lib/ci/migrate/database.rb | 67 ----------------------------- lib/ci/migrate/manager.rb | 72 ------------------------------- lib/ci/migrate/tags.rb | 42 ------------------ lib/tasks/ci/migrate.rake | 87 -------------------------------------- 6 files changed, 1 insertion(+), 297 deletions(-) delete mode 100644 lib/ci/migrate/builds.rb delete mode 100644 lib/ci/migrate/database.rb delete mode 100644 lib/ci/migrate/manager.rb delete mode 100644 lib/ci/migrate/tags.rb delete mode 100644 lib/tasks/ci/migrate.rake diff --git a/CHANGELOG b/CHANGELOG index e956d074d7..5d61040e58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.1.0 (unreleased) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) - Improved performance of the trending projects page + - Remove CI migration task - Improved performance of finding projects by their namespace - Fix bug where transferring a project would result in stale commit links (Stan Hu) - Include full path of source and target branch names in New Merge Request page (Stan Hu) diff --git a/lib/ci/migrate/builds.rb b/lib/ci/migrate/builds.rb deleted file mode 100644 index c4f62e5529..0000000000 --- a/lib/ci/migrate/builds.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Ci - module Migrate - class Builds - attr_reader :app_builds_dir, :backup_builds_tarball, :backup_dir - - def initialize - @app_builds_dir = Settings.gitlab_ci.builds_path - @backup_dir = Gitlab.config.backup.path - @backup_builds_tarball = File.join(backup_dir, 'builds/builds.tar.gz') - end - - def restore - backup_existing_builds_dir - - FileUtils.mkdir_p(app_builds_dir, mode: 0700) - unless system('tar', '-C', app_builds_dir, '-zxf', backup_builds_tarball) - abort 'Restore failed'.red - end - end - - def backup_existing_builds_dir - timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}") - if File.exists?(app_builds_dir) - FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path)) - end - end - end - end -end diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb deleted file mode 100644 index bf9b80f1f6..0000000000 --- a/lib/ci/migrate/database.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'yaml' - -module Ci - module Migrate - class Database - attr_reader :config - - def initialize - @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env] - end - - def restore - decompress_rd, decompress_wr = IO.pipe - decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name) - decompress_wr.close - - restore_pid = case config["adapter"] - when /^mysql/ then - $progress.print "Restoring MySQL database #{config['database']} ... " - # Workaround warnings from MySQL 5.6 about passwords on cmd line - ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] - spawn('mysql', *mysql_args, config['database'], in: decompress_rd) - when "postgresql" then - $progress.print "Restoring PostgreSQL database #{config['database']} ... " - pg_env - spawn('psql', config['database'], in: decompress_rd) - end - decompress_rd.close - - success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? } - abort 'Restore failed' unless success - end - - protected - - def db_file_name - File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') - end - - def mysql_args - args = { - 'host' => '--host', - 'port' => '--port', - 'socket' => '--socket', - 'username' => '--user', - 'encoding' => '--default-character-set' - } - args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact - end - - def pg_env - ENV['PGUSER'] = config["username"] if config["username"] - ENV['PGHOST'] = config["host"] if config["host"] - ENV['PGPORT'] = config["port"].to_s if config["port"] - ENV['PGPASSWORD'] = config["password"].to_s if config["password"] - end - - def report_success(success) - if success - puts '[DONE]'.green - else - puts '[FAILED]'.red - end - end - end - end -end diff --git a/lib/ci/migrate/manager.rb b/lib/ci/migrate/manager.rb deleted file mode 100644 index e5e4fb784e..0000000000 --- a/lib/ci/migrate/manager.rb +++ /dev/null @@ -1,72 +0,0 @@ -module Ci - module Migrate - class Manager - CI_IMPORT_PREFIX = '8.0' # Only allow imports from CI 8.0.x - - def cleanup - $progress.print "Deleting tmp directories ... " - - backup_contents.each do |dir| - next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) - - if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) - $progress.puts "done".green - else - puts "deleting tmp directory '#{dir}' failed".red - abort 'Backup failed' - end - end - end - - def unpack - Dir.chdir(Gitlab.config.backup.path) - - # check for existing backups in the backup dir - file_list = Dir.glob("*_gitlab_ci_backup.tar").each.map { |f| f.split(/_/).first.to_i } - puts "no backups found" if file_list.count == 0 - - if file_list.count > 1 && ENV["BACKUP"].nil? - puts "Found more than one backup, please specify which one you want to restore:" - puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" - exit 1 - end - - tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar") - - unless File.exists?(tar_file) - puts "The specified CI backup doesn't exist!" - exit 1 - end - - $progress.print "Unpacking backup ... " - - unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".red - exit 1 - else - $progress.puts "done".green - end - - ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 - - # restoring mismatching backups can lead to unexpected problems - if !settings[:gitlab_version].start_with?(CI_IMPORT_PREFIX) - puts "GitLab CI version mismatch:".red - puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI (#{settings[:gitlab_version]}) version in the backup!".red - exit 1 - end - end - - private - - def backup_contents - ["db", "builds", "backup_information.yml"] - end - - def settings - @settings ||= YAML.load_file("backup_information.yml") - end - end - end -end - diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb deleted file mode 100644 index 97e043ece2..0000000000 --- a/lib/ci/migrate/tags.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'yaml' - -module Ci - module Migrate - class Tags - def restore - puts 'Inserting tags...' - connection.select_all('SELECT ci_tags.name FROM ci_tags').each do |tag| - begin - connection.execute("INSERT INTO tags (name) VALUES(#{ActiveRecord::Base::sanitize(tag['name'])})") - rescue ActiveRecord::RecordNotUnique - end - end - - ActiveRecord::Base.transaction do - puts 'Deleting old taggings...' - connection.execute "DELETE FROM taggings WHERE context = 'tags' AND taggable_type LIKE 'Ci::%'" - - puts 'Inserting taggings...' - connection.execute( - 'INSERT INTO taggings (taggable_type, taggable_id, tag_id, context) ' + - "SELECT CONCAT('Ci::', ci_taggings.taggable_type), ci_taggings.taggable_id, tags.id, 'tags' FROM ci_taggings " + - 'JOIN ci_tags ON ci_tags.id = ci_taggings.tag_id ' + - 'JOIN tags ON tags.name = ci_tags.name ' - ) - - puts 'Resetting counters... ' - connection.execute( - 'UPDATE tags SET ' + - 'taggings_count = (SELECT COUNT(*) FROM taggings WHERE tags.id = taggings.tag_id)' - ) - end - end - - protected - - def connection - ActiveRecord::Base.connection - end - end - end -end diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake deleted file mode 100644 index 1de664c85e..0000000000 --- a/lib/tasks/ci/migrate.rake +++ /dev/null @@ -1,87 +0,0 @@ -namespace :ci do - desc 'GitLab | Import and migrate CI database' - task migrate: :environment do - warn_user_is_not_gitlab - configure_cron_mode - - unless ENV['force'] == 'yes' - puts 'This will remove all CI related data and restore it from the provided backup.' - ask_to_continue - puts '' - end - - # disable CI for time of migration - enable_ci(false) - - # unpack archives - migrate = Ci::Migrate::Manager.new - migrate.unpack - - Rake::Task['ci:migrate:db'].invoke - Rake::Task['ci:migrate:builds'].invoke - Rake::Task['ci:migrate:tags'].invoke - Rake::Task['ci:migrate:services'].invoke - - # enable CI for time of migration - enable_ci(true) - - migrate.cleanup - end - - namespace :migrate do - desc 'GitLab | Import CI database' - task db: :environment do - configure_cron_mode - $progress.puts 'Restoring database ... '.blue - Ci::Migrate::Database.new.restore - $progress.puts 'done'.green - end - - desc 'GitLab | Import CI builds' - task builds: :environment do - configure_cron_mode - $progress.puts 'Restoring builds ... '.blue - Ci::Migrate::Builds.new.restore - $progress.puts 'done'.green - end - - desc 'GitLab | Migrate CI tags' - task tags: :environment do - configure_cron_mode - $progress.puts 'Migrating tags ... '.blue - ::Ci::Migrate::Tags.new.restore - $progress.puts 'done'.green - end - - desc 'GitLab | Migrate CI auto-increments' - task autoincrements: :environment do - c = ActiveRecord::Base.connection - c.tables.select { |t| t.start_with?('ci_') }.each do |table| - result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1") - if result - ai_val = result['id'].to_i + 1 - puts "Resetting auto increment ID for #{table} to #{ai_val}" - if c.adapter_name == 'PostgreSQL' - c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}") - else - c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}") - end - end - end - end - - desc 'GitLab | Migrate CI services' - task services: :environment do - $progress.puts 'Migrating services ... '.blue - c = ActiveRecord::Base.connection - c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'") - $progress.puts 'done'.green - end - end - - def enable_ci(enabled) - settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults - settings.ci_enabled = enabled - settings.save! - end -end From 0179d5df18dccee6f44908b91a9975352e652498 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Oct 2015 12:15:39 +0200 Subject: [PATCH 229/443] Update SourceSansPro fonts Signed-off-by: Dmitriy Zaporozhets --- app/assets/fonts/SourceSansPro-Black.ttf | Bin 148368 -> 289364 bytes app/assets/fonts/SourceSansPro-BlackIt.ttf | Bin 0 -> 103404 bytes app/assets/fonts/SourceSansPro-Bold.ttf | Bin app/assets/fonts/SourceSansPro-BoldIt.ttf | Bin 0 -> 103608 bytes app/assets/fonts/SourceSansPro-ExtraLight.ttf | Bin 150528 -> 291652 bytes .../fonts/SourceSansPro-ExtraLightIt.ttf | Bin 0 -> 104768 bytes app/assets/fonts/SourceSansPro-It.ttf | Bin 0 -> 104236 bytes app/assets/fonts/SourceSansPro-Light.ttf | Bin app/assets/fonts/SourceSansPro-LightIt.ttf | Bin 0 -> 104616 bytes app/assets/fonts/SourceSansPro-Regular.ttf | Bin app/assets/fonts/SourceSansPro-Semibold.ttf | Bin app/assets/fonts/SourceSansPro-SemiboldIt.ttf | Bin 0 -> 104020 bytes 12 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Black.ttf create mode 100644 app/assets/fonts/SourceSansPro-BlackIt.ttf mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Bold.ttf create mode 100644 app/assets/fonts/SourceSansPro-BoldIt.ttf mode change 100755 => 100644 app/assets/fonts/SourceSansPro-ExtraLight.ttf create mode 100644 app/assets/fonts/SourceSansPro-ExtraLightIt.ttf create mode 100644 app/assets/fonts/SourceSansPro-It.ttf mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Light.ttf create mode 100644 app/assets/fonts/SourceSansPro-LightIt.ttf mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Regular.ttf mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Semibold.ttf create mode 100644 app/assets/fonts/SourceSansPro-SemiboldIt.ttf diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf old mode 100755 new mode 100644 index cb89a2d171e4db582ffaae8b5fe465680921996b..9c9b5cb7f03d4240a9ee0323aa2c4c4ec7689fda GIT binary patch literal 289364 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%0DNaPN-24sqMQfw*W>cwx5y@^;8uvUfaNnh;`zF%s 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 NrUb<~kHa9y{{zE0D)9gS literal 148368 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

%@p18YD)=wQ{1J~Sj-S}sZox=S{CZaND(_! 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

F+#hw9N)PA=S#kFOR8fDW=~}~dbW>%l;*4(m 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

3PcFjP6|Hnj> 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 Zb-1#s=G{fHulI=mF60-tW9S6={{U@`BJBVG literal 150528 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

*0fSY=GawYBfnK(*Pfl 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

JQ8vKU?Xy0C8` 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 OxMkm*Fl_)fLH-MfLoMI{ literal 0 HcmV?d00001 diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f7af53775951654ea308d497aadd200237d87e56 GIT binary patch literal 104236 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 literal 0 HcmV?d00001 diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f18827985ef836a047e5f292a5c5edb52189f2b5 GIT binary patch literal 104616 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 literal 0 HcmV?d00001 From c21d46c29ffe2547cb454c2658fa9644a19f1dd8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Oct 2015 12:34:09 +0200 Subject: [PATCH 230/443] Fix build trace updating --- CHANGELOG | 1 + app/assets/javascripts/ci/build.coffee | 2 +- app/views/projects/builds/show.html.haml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4a9a85d5eb..4881d4e9e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ v 8.1.0 (unreleased) - Improved performance of the trending projects page - Improved performance of finding projects by their namespace - Fix bug where transferring a project would result in stale commit links (Stan Hu) + - Fix build trace updating - Include full path of source and target branch names in New Merge Request page (Stan Hu) - Add user preference to view activities as default dashboard (Stan Hu) - Add option to admin area to sign in as a specific user (Pavel Forkert) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index c30859b484..93385b32a1 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -22,7 +22,7 @@ class CiBuild # Only valid for runnig build when output changes during time # CiBuild.interval = setInterval => - if window.location.href is build_url + if window.location.href.split("#").first() is build_url $.ajax url: build_url dataType: "json" diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c45bfb27b8..3a8172dc8e 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -175,4 +175,4 @@ :javascript - new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") From c192444db5ab4b770ec081b48229ee50b6eaa57e Mon Sep 17 00:00:00 2001 From: "Artem V. Navrotskiy" Date: Tue, 20 Oct 2015 21:47:29 +0300 Subject: [PATCH 231/443] Show empty repository page if repository don't have branches --- CHANGELOG | 1 + app/models/project.rb | 2 +- app/models/repository.rb | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4a9a85d5eb..bb0e97603c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -88,6 +88,7 @@ v 8.0.5 - Correct lookup-by-email for LDAP logins - Fix loading spinner sometimes not being hidden on Merge Request tab switches - Animate the logo on hover + - Show "Empty Repository Page" for repository without branches (#9728) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/app/models/project.rb b/app/models/project.rb index 88cd88dcb5..4c5bf220f2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -567,7 +567,7 @@ class Project < ActiveRecord::Base end def empty_repo? - !repository.exists? || repository.empty? + !repository.exists? || !repository.has_visible_content? end def repo diff --git a/app/models/repository.rb b/app/models/repository.rb index 0808896fd8..88d3d73a40 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -44,6 +44,19 @@ class Repository raw_repository.empty? end + # + # Git repository can contains some hidden refs like: + # /refs/notes/* + # /refs/git-as-svn/* + # /refs/pulls/* + # This refs by default not visible in project page and not cloned to client side. + # + # This method return true if repository contains some content visible in project page. + # + def has_visible_content? + !raw_repository.branches.empty? + end + def commit(id = 'HEAD') return nil unless raw_repository commit = Gitlab::Git::Commit.find(raw_repository, id) From d7bcfe4fc020529f9b6ed2a75dfb2f31acded080 Mon Sep 17 00:00:00 2001 From: Dirceu Pereira Tiegs Date: Wed, 14 Oct 2015 20:14:24 -0300 Subject: [PATCH 232/443] Fix issue #3055 (project search with unmatched parentheses) --- app/models/repository.rb | 2 +- lib/gitlab/project_search_results.rb | 2 +- spec/lib/gitlab/project_search_results_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 8b51602bc2..04e1114608 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -482,7 +482,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref}) + args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 0dab7bcfa4..0a2be605af 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -9,7 +9,7 @@ module Gitlab else nil end - @query = Shellwords.shellescape(query) if query.present? + @query = query end def objects(scope, page = nil) diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 32a25f08ca..19327ac8ce 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ProjectSearchResults do it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to be_nil } - it { expect(results.query).to eq('hello\\ world') } + it { expect(results.query).to eq('hello world') } end describe 'initialize with ref' do @@ -18,6 +18,6 @@ describe Gitlab::ProjectSearchResults do it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to eq(ref) } - it { expect(results.query).to eq('hello\\ world') } + it { expect(results.query).to eq('hello world') } end end From 4eac37fd872db5e1b73d3d74b943a943ae7e1d6a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Oct 2015 23:33:49 -0700 Subject: [PATCH 233/443] Don't show "Add README" link in an empty repository if user doesn't have access to push Closes #3094 --- CHANGELOG | 1 + app/views/projects/empty.html.haml | 85 ++++++++++++++++-------------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c1933412ae..281ec1b9bc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.1.0 (unreleased) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu) + - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index e06454fd14..c3858e78ca 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -2,53 +2,56 @@ - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' - + = render "home_panel" .gray-content-block.center %h3.page-title The repository for this project is empty - %p - If you already have files you can push them using command line instructions below. - %br - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' - file to this project. + - if can?(current_user, :download_code, @project) + %p + If you already have files you can push them using command line instructions below. + %br + - if can?(current_user, :push_code, @project) + Otherwise you can start with + = link_to "adding README", new_readme_path, class: 'underlined-link' + file to this project. -.prepend-top-20 -.empty_wrapper - %h3.page-title-empty - Command line instructions - %div.git-empty - %fieldset - %h5 Git global setup - %pre.light-well - :preserve - git config --global user.name "#{h git_user_name}" - git config --global user.email "#{h git_user_email}" +- if can?(current_user, :download_code, @project) + .prepend-top-20 + .empty_wrapper + %h3.page-title-empty + Command line instructions + %div.git-empty + %fieldset + %h5 Git global setup + %pre.light-well + :preserve + git config --global user.name "#{h git_user_name}" + git config --global user.email "#{h git_user_email}" - %fieldset - %h5 Create a new repository - %pre.light-well - :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} - cd #{h @project.path} - touch README.md - git add README.md - git commit -m "add README" - git push -u origin master + %fieldset + %h5 Create a new repository + %pre.light-well + :preserve + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + cd #{h @project.path} + touch README.md + git add README.md + git commit -m "add README" + git push -u origin master - %fieldset - %h5 Existing folder or Git repository - %pre.light-well - :preserve - cd existing_folder - git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git add . - git commit - git push -u origin master + %fieldset + %h5 Existing folder or Git repository + %pre.light-well + :preserve + cd existing_folder + git init + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit + git push -u origin master - - if can? current_user, :remove_project, @project - .prepend-top-20 - = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + - if can? current_user, :remove_project, @project + .prepend-top-20 + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" From 9c61b73c21f70b3f4a7e3e8f4b7e4d8ad0544094 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 17:24:09 +0200 Subject: [PATCH 234/443] Make sure MR refresh service correctly determines newly added commits. --- app/services/merge_requests/refresh_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 121f689901..2808b98a4a 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -7,6 +7,8 @@ module MergeRequests @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = [] + + reload_merge_requests # Leave a system note if a branch were deleted/added if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) @@ -18,7 +20,6 @@ module MergeRequests close_merge_requests end - reload_merge_requests execute_mr_web_hooks true From 464c939a7e55f9677b5c1f25b50c31f7d10660ea Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Oct 2015 17:34:12 +0200 Subject: [PATCH 235/443] Clean up MR refresh service somewhat. --- .../merge_requests/refresh_service.rb | 74 ++++++++++++------- app/services/system_note_service.rb | 2 +- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 2808b98a4a..d68bc79ecc 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -5,17 +5,15 @@ module MergeRequests @oldrev, @newrev = oldrev, newrev @branch_name = Gitlab::Git.ref_name(ref) - @fork_merge_requests = @project.fork_merge_requests.opened - @commits = [] - + + find_new_commits reload_merge_requests - # Leave a system note if a branch were deleted/added - if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) + # Leave a system note if a branch was deleted/added + if branch_added? || branch_removed? comment_mr_branch_presence_changed - comment_mr_with_commits if @commits.present? + comment_mr_with_commits else - @commits = @project.repository.commits_between(oldrev, newrev) comment_mr_with_commits close_merge_requests end @@ -55,7 +53,7 @@ module MergeRequests # Note: we should update merge requests from forks too def reload_merge_requests merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a - merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a + merge_requests += fork_merge_requests.by_branch(@branch_name).to_a merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| @@ -78,29 +76,37 @@ module MergeRequests end end - # Add comment about branches being deleted or added to merge requests - def comment_mr_branch_presence_changed - presence = Gitlab::Git.blank_ref?(@oldrev) ? :add : :delete + def find_new_commits + if branch_added? + @commits = [] + + merge_request = merge_requests_for_source_branch.first + return unless merge_request - merge_requests_for_source_branch.each do |merge_request| last_commit = merge_request.last_commit - # Only look at changed commits in restore branch case - unless Gitlab::Git.blank_ref?(@newrev) - begin - # Since any number of commits could have been made to the restored branch, - # find the common root to see what has been added. - common_ref = @project.repository.merge_base(last_commit.id, @newrev) - # If the a commit no longer exists in this repo, gitlab_git throws - # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 - @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref - rescue - end - - # Prevent system notes from seeing a blank SHA - @oldrev = nil + begin + # Since any number of commits could have been made to the restored branch, + # find the common root to see what has been added. + common_ref = @project.repository.merge_base(last_commit.id, @newrev) + # If the a commit no longer exists in this repo, gitlab_git throws + # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 + @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref + rescue end + elsif branch_removed? + # No commits for a deleted branch. + @commits = [] + else + @commits = @project.repository.commits_between(@oldrev, @newrev) + end + end + # Add comment about branches being deleted or added to merge requests + def comment_mr_branch_presence_changed + presence = branch_added? ? :add : :delete + + merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, :source, @branch_name, presence) @@ -109,6 +115,8 @@ module MergeRequests # Add comment about pushing new commits to merge requests def comment_mr_with_commits + return unless @commits.present? + merge_requests_for_source_branch.each do |merge_request| mr_commit_ids = Set.new(merge_request.commits.map(&:id)) @@ -136,9 +144,21 @@ module MergeRequests def merge_requests_for_source_branch @source_merge_requests ||= begin merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a + merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a filter_merge_requests(merge_requests) end end + + def fork_merge_requests + @fork_merge_requests ||= @project.fork_merge_requests.opened + end + + def branch_added? + Gitlab::Git.blank_ref?(@oldrev) + end + + def branch_removed? + Gitlab::Git.blank_ref?(@newrev) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 37f454cfc3..708c2f0048 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -327,7 +327,7 @@ class SystemNoteService commit_ids = if count == 1 existing_commits.first.short_id else - if oldrev + if oldrev && !Gitlab::Git.blank_ref?(oldrev) "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" else "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" From 38a93a25f394283d36d5d4e2f4bc5166a6dfbedd Mon Sep 17 00:00:00 2001 From: Sander Boom Date: Wed, 21 Oct 2015 22:52:17 +0200 Subject: [PATCH 236/443] Fixed linting error 'variables should be a map of key-valued strings' --- doc/ci/docker/using_docker_images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 191e3a8144..ef8a7ec1e8 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -90,7 +90,7 @@ you need to set MYSQL_ALLOW_EMPTY_PASSWORD. - mysql variables: - MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ``` For other possible configuration variables check the From 05cb65dc1f816a75677d0db401e29a9b640cf4d7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 10:18:44 +0200 Subject: [PATCH 237/443] Fix Gitlab::Database#mysql? --- lib/gitlab/database.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 741a52714a..71f37f1fef 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -1,7 +1,7 @@ module Gitlab module Database def self.mysql? - ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' + ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2' end def self.postgresql? From 98f982f91d6e2d6ec1b59ea5645d8320989e2de6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 10:19:12 +0200 Subject: [PATCH 238/443] Only postgres does case sensitive compares --- spec/controllers/projects_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 9b49f60b66..4bb47c6b02 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -70,7 +70,7 @@ describe ProjectsController do # MySQL queries are case insensitive by default, so this spec would fail. - unless Gitlab::Database.mysql? + if Gitlab::Database.postgresql? context "when there is also a match with the same casing" do let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) } From 57382d6170a637c30d84efc11f6d7bd111cd6ca9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 10:25:17 +0200 Subject: [PATCH 239/443] Fix changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bb0e97603c..eb5a704f48 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 8.2.0 (unreleased) - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) - Improved performance of sorting milestone issues - Allow users to select the Files view as default project view (Cristian Bica) + - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy) v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) @@ -88,7 +89,6 @@ v 8.0.5 - Correct lookup-by-email for LDAP logins - Fix loading spinner sometimes not being hidden on Merge Request tab switches - Animate the logo on hover - - Show "Empty Repository Page" for repository without branches (#9728) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From 2305483d7d0f398caedabf6a99a94913be75138a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 22 Oct 2015 10:38:47 +0200 Subject: [PATCH 240/443] Require jobs to be named --- CHANGELOG | 1 + lib/ci/gitlab_ci_yaml_processor.rb | 38 ++++++++++++-------- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 14 ++++++++ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 671955d70c..aa70d49435 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.1.0 (unreleased) - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x + - Require CI jobs to be named - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 0da73e387e..efcd2faffc 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -139,66 +139,74 @@ module Ci end @jobs.each do |name, job| - validate_job!("#{name} job", job) + validate_job!(name, job) end true end def validate_job!(name, job) + if name.blank? || !validate_string(name) + raise ValidationError, "job name should be non-empty string" + end + job.keys.each do |key| unless ALLOWED_JOB_KEYS.include? key - raise ValidationError, "#{name}: unknown parameter #{key}" + raise ValidationError, "#{name} job: unknown parameter #{key}" end end - if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script]) - raise ValidationError, "#{name}: script should be a string or an array of a strings" + if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) + raise ValidationError, "#{name} job: script should be a string or an array of a strings" end if job[:stage] unless job[:stage].is_a?(String) && job[:stage].in?(stages) - raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}" + raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}" end end - if job[:image] && !job[:image].is_a?(String) - raise ValidationError, "#{name}: image should be a string" + if job[:image] && !validate_string(job[:image]) + raise ValidationError, "#{name} job: image should be a string" end if job[:services] && !validate_array_of_strings(job[:services]) - raise ValidationError, "#{name}: services should be an array of strings" + raise ValidationError, "#{name} job: services should be an array of strings" end if job[:tags] && !validate_array_of_strings(job[:tags]) - raise ValidationError, "#{name}: tags parameter should be an array of strings" + raise ValidationError, "#{name} job: tags parameter should be an array of strings" end if job[:only] && !validate_array_of_strings(job[:only]) - raise ValidationError, "#{name}: only parameter should be an array of strings" + raise ValidationError, "#{name} job: only parameter should be an array of strings" end if job[:except] && !validate_array_of_strings(job[:except]) - raise ValidationError, "#{name}: except parameter should be an array of strings" + raise ValidationError, "#{name} job: except parameter should be an array of strings" end if job[:allow_failure] && !job[:allow_failure].in?([true, false]) - raise ValidationError, "#{name}: allow_failure parameter should be an boolean" + raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" end if job[:when] && !job[:when].in?(%w(on_success on_failure always)) - raise ValidationError, "#{name}: when parameter should be on_success, on_failure or always" + raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always" end end private def validate_array_of_strings(values) - values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)} + values.is_a?(Array) && values.all? { |value| validate_string(value) } end def validate_variables(variables) - variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)} + variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) } + end + + def validate_string(value) + value.is_a?(String) || value.is_a?(Symbol) end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 2260a6f813..abdb6b89ac 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -218,6 +218,20 @@ module Ci end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string") end + it "returns errors if job name is blank" do + config = YAML.dump({ '' => { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end + + it "returns errors if job name is non-string" do + config = YAML.dump({ 10 => { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end + it "returns errors if job image parameter is invalid" do config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do From c7c5bd48434d3f002010032ebd2753352d5e9e87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 10:46:54 +0200 Subject: [PATCH 241/443] Change dashboard snippet index menu class to match test --- app/views/dashboard/snippets/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 2b67fd05b9..07b6d57932 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -9,7 +9,7 @@ = icon('plus') New Snippet - .btn-group.btn-group-next.event-filter + .btn-group.btn-group-next.snippet-scope-menu = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do All %span.badge From ae9f81a9501f401133c9ba70e84a2148cd92f1ee Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 22 Oct 2015 11:00:48 +0200 Subject: [PATCH 242/443] Update CHANGELOG [ci skip] --- CHANGELOG | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 671955d70c..9a550ca113 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Improved performance of replacing references in comments - - Fix duplicate repositories in GitHub import page (Stan Hu) - - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw) @@ -12,7 +9,10 @@ v 8.2.0 (unreleased) - Allow users to select the Files view as default project view (Cristian Bica) - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy) -v 8.1.0 (unreleased) +v 8.1.0 + - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) + - Fix duplicate repositories in GitHub import page (Stan Hu) + - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. @@ -87,11 +87,11 @@ v 8.1.0 (unreleased) - Let gitlab-git-http-server generate and serve 'git archive' downloads - Optimize query when filtering on issuables (Zeger-Jan van de Weg) - Fix padding of outdated discussion item. + - Animate the logo on hover v 8.0.5 - Correct lookup-by-email for LDAP logins - Fix loading spinner sometimes not being hidden on Merge Request tab switches - - Animate the logo on hover v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From 40934ae39bc9aea6ffb176f96bba72fa688de837 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 22 Oct 2015 11:13:19 +0200 Subject: [PATCH 243/443] Fix 500 when editing CI services --- CHANGELOG | 1 + app/controllers/projects/ci_services_controller.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9a550ca113..e4e58a3e83 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.1.0 - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) - Improved performance of finding users by username or Email address + - Fix 500 when editing CI service - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu) - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb index 406f313ae7..550a019e8e 100644 --- a/app/controllers/projects/ci_services_controller.rb +++ b/app/controllers/projects/ci_services_controller.rb @@ -14,17 +14,17 @@ class Projects::CiServicesController < Projects::ApplicationController end def update - if @service.update_attributes(service_params) - redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param) + if service.update_attributes(service_params) + redirect_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) else render 'edit' end end def test - last_build = @project.builds.last + last_build = @project.ci_builds.last - if @service.execute(last_build) + if service.execute(last_build) message = { notice: 'We successfully tested the service' } else message = { alert: 'We tried to test the service but error occurred' } From 337b6632b643e4eb0417e42302a347ea87a2315c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 22 Oct 2015 11:31:00 +0200 Subject: [PATCH 244/443] Fix CSS class for runner status --- CHANGELOG | 1 + app/assets/stylesheets/pages/runners.scss | 52 +++++++++++------------ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9a550ca113..ebee45ace9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) + - Fix CSS for runner status - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 2b15ab8312..a9111a7388 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -1,36 +1,34 @@ -.ci-body { - .runner-state { - padding: 6px 12px; - margin-right: 10px; - color: #FFF; +.runner-state { + padding: 6px 12px; + margin-right: 10px; + color: #FFF; - &.runner-state-shared { - background: #32b186; - } - &.runner-state-specific { - background: #3498db; - } + &.runner-state-shared { + background: #32b186; } + &.runner-state-specific { + background: #3498db; + } +} + +.runner-status-online { + color: green; +} - .runner-status-online { - color: green; - } +.runner-status-offline { + color: gray; +} - .runner-status-offline { - color: gray; - } +.runner-status-paused { + color: red; +} - .runner-status-paused { - color: red; +.runner { + .btn { + padding: 1px 6px; } - - .runner { - .btn { - padding: 1px 6px; - } - h4 { - font-weight: normal; - } + h4 { + font-weight: normal; } } From 3c3e57705f801c5a810183523c5898d44d5de872 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 22 Oct 2015 14:22:49 +0200 Subject: [PATCH 245/443] Add builds to the docs as a valid skip option for backups. --- doc/raketasks/backup_restore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 06f582dcee..606532a6fb 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -29,7 +29,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, -uploads (attachments), repositories. Use a comma to specify several options at the same time. +uploads (attachments), repositories, builds(CI build output logs). Use a comma to specify several options at the same time. ``` sudo gitlab-rake gitlab:backup:create SKIP=db,uploads From 5f1c99b757b7c23d476b6b523160c242db09259f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 22 Oct 2015 16:08:45 +0200 Subject: [PATCH 246/443] Fix alignment of the filter counter for Issuables --- app/assets/stylesheets/pages/issuable.scss | 8 ++++++++ app/views/projects/issues/_issues.html.haml | 7 ++++--- .../projects/merge_requests/_merge_requests.html.haml | 6 ++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9da085a347..c60aa5c7fe 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -80,3 +80,11 @@ } } } + +.issuable-filter-count { + span { + display: block; + margin-bottom: -16px; + padding: 13px 0; + } +} diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index a3399c57aa..ca5b1a8386 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -5,8 +5,9 @@ .nothing-here-block No issues to show - if @issues.present? - .pull-right - %span.issue_counter #{@issues.total_count} - issues for this filter + .issuable-filter-count + %span.pull-right + = @issues.total_count + issues for this filter = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index d86707b3d9..0af970e4b9 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,8 +5,10 @@ .nothing-here-block No merge requests to show - if @merge_requests.present? - .pull-right - %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter + .issuable-filter-count + %span.pull-right + = @merge_requests.total_count + merge requests for this filter = paginate @merge_requests, theme: "gitlab" From 2db7868a55570dd1a635725c7e8420fc8a8a4379 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Oct 2015 16:50:21 +0200 Subject: [PATCH 247/443] Make file list more compact Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/tree.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 1b0cef481d..d4ab6967cc 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -5,7 +5,7 @@ tr { > td, > th { - line-height: 32px; + line-height: 28px; } &:hover { From 097238f0a5f6735b7f0596e2283068be17d39e91 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Oct 2015 17:11:21 +0200 Subject: [PATCH 248/443] Restore edit button in project readme --- app/assets/stylesheets/pages/projects.scss | 8 ++++++++ app/views/projects/_readme.html.haml | 10 ++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 41bea0ec5c..27bedea0e1 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -544,5 +544,13 @@ pre.light-well { } .project-show-readme .readme-holder { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + padding: $gl-padding; border-top: 0; + + .edit-project-readme { + z-index: 100; + position: relative; + } } diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 0a1cecfdcd..b5ef0aca54 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -1,10 +1,8 @@ - if readme = @repository.readme - %article.file-holder.readme-holder - .file-title - = blob_icon readme.mode, readme.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do - %strong - = readme.name + %article.readme-holder + .pull-right + - if can?(current_user, :push_code, @project) + = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme' .file-content.wiki = cache(readme_cache_key) do = render_readme(readme) From 8655f4793e14b1361ca7f1900a74390b21cb2b1d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Oct 2015 17:25:06 +0200 Subject: [PATCH 249/443] Remove bad css style Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/mixins.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1d79834489..fe078d016d 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -152,8 +152,3 @@ } } } - -.fa-align { - top: 20px; - position: relative; -} From ddb984470d097d4296b6f23fb533429b23d7d7b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Oct 2015 17:33:51 +0200 Subject: [PATCH 250/443] Add extra padding to README block Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 27bedea0e1..8dd782b624 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -546,9 +546,9 @@ pre.light-well { .project-show-readme .readme-holder { margin-left: -$gl-padding; margin-right: -$gl-padding; - padding: $gl-padding; + padding: ($gl-padding + 7px); border-top: 0; - + .edit-project-readme { z-index: 100; position: relative; From f61dd16c3f9a0035b0579677303ac44b3cf7848e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Oct 2015 17:58:09 +0200 Subject: [PATCH 251/443] Improve css code quality for projects.scss Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 29 +++++++++---------- .../projects/buttons/_notifications.html.haml | 4 +-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 27bedea0e1..dfb25dc20a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -50,7 +50,17 @@ } .project-home-dropdown { - margin: 11px 3px 0; + margin: 13px 0px 0; + } + + .notifications-btn { + .fa-bell { + margin-right: 6px; + } + + .fa-angle-down { + margin-left: 6px; + } } .project-home-desc { @@ -85,6 +95,7 @@ color: inherit; } } + .input-group { display: inline-table; position: relative; @@ -233,23 +244,11 @@ } } - .fa-fw { + i { margin-right: 8px; } } -.fa-bell { - margin-right: 6px; -} - -.fa-angle-down { - margin-left: 6px; -} - -.project-home-panel .project-home-dropdown { - margin: 13px 0px 0; -} - .project-visibility-level-holder { .radio { margin-bottom: 10px; @@ -548,7 +547,7 @@ pre.light-well { margin-right: -$gl-padding; padding: $gl-padding; border-top: 0; - + .edit-project-readme { z-index: 100; position: relative; diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 0c29884491..3e83ec3912 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -5,7 +5,7 @@ = hidden_field_tag :notification_id, @membership.id = hidden_field_tag :notification_level %span.dropdown - %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') = notification_label(@membership) = icon('angle-down') @@ -14,7 +14,7 @@ = notification_list_item(level, @membership) - when GroupMember - .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} + .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} = icon('bell') = notification_label(@membership) = icon('angle-down') From 835f6a2b51f43c65eb70c6e9e764263786ed9f8e Mon Sep 17 00:00:00 2001 From: "Artem V. Navrotskiy" Date: Thu, 22 Oct 2015 19:02:55 +0300 Subject: [PATCH 252/443] Bump gitlab-shell to v2.6.6 --- GITLAB_SHELL_VERSION | 2 +- doc/install/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 57cf282ebb..338a5b5d8f 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.5 +2.6.6 diff --git a/doc/install/installation.md b/doc/install/installation.md index 2e9ac7393e..a470033f9a 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -310,7 +310,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.6] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: From 95df86638d364a87469550cce852871634ace262 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 22 Oct 2015 18:38:00 +0200 Subject: [PATCH 253/443] Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork --- CHANGELOG | 1 + app/controllers/projects/commits_controller.rb | 2 +- app/models/merge_request.rb | 2 +- spec/models/merge_request_spec.rb | 6 ++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3ca30375f6..8cefa04311 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.2.0 (unreleased) - Improved performance of sorting milestone issues - Allow users to select the Files view as default project view (Cristian Bica) - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy) + - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index d1c15174ae..58fb946dbc 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -12,7 +12,7 @@ class Projects::CommitsController < Projects::ApplicationController @limit, @offset = (params[:limit] || 40), (params[:offset] || 0) @commits = @repo.commits(@ref, @path, @limit, @offset) - @note_counts = Note.where(commit_id: @commits.map(&:id)). + @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count respond_to do |format| diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 21861a46a8..8d9ad44681 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -257,7 +257,7 @@ class MergeRequest < ActiveRecord::Base Note.where( "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + - "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", + "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", mr_id: id, commit_ids: commit_ids, target_project_id: target_project_id, diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6aaf1c036b..eed2cbc541 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -79,6 +79,12 @@ describe MergeRequest do expect(merge_request.commits).not_to be_empty expect(merge_request.mr_and_commit_notes.count).to eq(2) end + + it "should include notes for commits from target project as well" do + create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project) + expect(merge_request.commits).not_to be_empty + expect(merge_request.mr_and_commit_notes.count).to eq(3) + end end describe '#is_being_reassigned?' do From 64a270a6455d474926aaa8be281c88fca072a113 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 22 Oct 2015 13:53:12 -0700 Subject: [PATCH 254/443] Fix cloning Wiki repositories via HTTP Cloning a project Wiki over HTTP would end up cloning the main repository since the .wiki extension was being stripped. Closes #3106 --- CHANGELOG | 1 + lib/gitlab/backend/grack_auth.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fa630f354c..6d2e55dbbd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Fix cloning Wiki repositories via HTTP (Stan Hu) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 6830a916bc..85a2d1a93a 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -193,12 +193,19 @@ module Grack end def render_grack_auth_ok + repo_path = + if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/ + ProjectWiki.new(project).repository.path_to_repo + else + project.repository.path_to_repo + end + [ 200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), - 'RepoPath' => project.repository.path_to_repo, + 'RepoPath' => repo_path, })] ] end From 69e5e260b0bd28cd48983494b2765e1cef0d3c38 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 22 Oct 2015 14:16:37 -0700 Subject: [PATCH 255/443] Add spec for cloning Wiki over HTTP --- spec/lib/gitlab/backend/grack_auth_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index 37c527221a..dfa0e10318 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -50,6 +50,22 @@ describe Grack::Auth do end end + context "when the Wiki for a project exists" do + before do + @wiki = ProjectWiki.new(project) + env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs" + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + it "responds with the right project" do + response = auth.call(env) + json_body = ActiveSupport::JSON.decode(response[2][0]) + + expect(response.first).to eq(200) + expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace) + end + end + context "when the project exists" do before do env["PATH_INFO"] = project.path_with_namespace + ".git" From 3d613fe1e87a4e9837239b34f5fdf88063ea98f9 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 22 Oct 2015 17:16:51 +0800 Subject: [PATCH 256/443] Fix API::APIHelpers -> API::Helpers; Rails Autoload find file to require is use , APIHelpers -> api_helpers.rb, not helpers.rb; --- lib/api/api.rb | 2 +- lib/api/helpers.rb | 2 +- lib/ci/api/api.rb | 2 +- spec/requests/api/api_helpers_spec.rb | 26 +++++++++++++------------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/api/api.rb b/lib/api/api.rb index afc0402f9e..40671e2517 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -25,7 +25,7 @@ module API format :json content_type :txt, "text/plain" - helpers APIHelpers + helpers Helpers mount Groups mount GroupMembers diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 549b1f9e9a..652bdf9b27 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,5 +1,5 @@ module API - module APIHelpers + module Helpers PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER ="HTTP_SUDO" diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 218d8c3adc..0a4cbf69b6 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -26,7 +26,7 @@ module Ci format :json helpers Helpers - helpers ::API::APIHelpers + helpers ::API::Helpers mount Builds mount Commits diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 4048c29701..0c19094ec5 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe API, api: true do - include API::APIHelpers + include API::Helpers include ApiHelpers let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -13,25 +13,25 @@ describe API, api: true do def set_env(token_usr, identifier) clear_env clear_param - env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token - env[API::APIHelpers::SUDO_HEADER] = identifier + env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token + env[API::Helpers::SUDO_HEADER] = identifier end def set_param(token_usr, identifier) clear_env clear_param - params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token - params[API::APIHelpers::SUDO_PARAM] = identifier + params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token + params[API::Helpers::SUDO_PARAM] = identifier end def clear_env - env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER) - env.delete(API::APIHelpers::SUDO_HEADER) + env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::Helpers::SUDO_HEADER) end def clear_param - params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM) - params.delete(API::APIHelpers::SUDO_PARAM) + params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::Helpers::SUDO_PARAM) end def error!(message, status) @@ -40,22 +40,22 @@ describe API, api: true do describe ".current_user" do it "should return nil for an invalid token" do - env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } expect(current_user).to be_nil end it "should return nil for a user without access" do - env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false) expect(current_user).to be_nil end it "should leave user as is when sudo not specified" do - env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token expect(current_user).to eq(user) clear_env - params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token expect(current_user).to eq(user) end From 127836dd541ce0ecd4976d002d97b3e9e57f4947 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 11:40:57 +0200 Subject: [PATCH 257/443] Fix small CI UI regressions --- CHANGELOG | 2 ++ app/assets/javascripts/ci/build.coffee | 2 +- app/controllers/projects/builds_controller.rb | 9 +++++---- app/models/commit_status.rb | 1 - app/views/ci/lints/_create.html.haml | 7 ++++++- app/views/projects/builds/show.html.haml | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa630f354c..e81b5f74b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,8 @@ v 8.1.0 - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x - Require CI jobs to be named + - Fix CI rendering regressions + - Allow developer to manage builds - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 93385b32a1..44d5ddb7d9 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -31,7 +31,7 @@ class CiBuild $('#build-trace code').html build.trace_html $('#build-trace code').append '' @checkAutoscroll() - else + else if build.status != build_status Turbolinks.visit build_url , 4000 diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 816012762c..ad0adc1786 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -9,16 +9,17 @@ class Projects::BuildsController < Projects::ApplicationController def index @scope = params[:scope] @all_builds = project.ci_builds + @builds = @all_builds.order('created_at DESC') @builds = case @scope when 'all' - @all_builds + @builds when 'finished' - @all_builds.finished + @builds.finished else - @all_builds.running_or_pending + @builds.running_or_pending.reverse_order end - @builds = @builds.order('created_at DESC').page(params[:page]).per(30) + @builds = @builds.page(params[:page]).per(30) end def cancel_all diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8188ba3a28..0b73ab6d2e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -20,7 +20,6 @@ class CommitStatus < ActiveRecord::Base scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } - scope :running_or_pending, -> { where(status: [:running, :pending]) } state_machine :status, initial: :pending do event :run do diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index f45cd05aec..809fc52dbd 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -17,7 +17,7 @@ %td #{stage.capitalize} Job - #{build[:name]} %td %pre - = simple_format build[:script] + = simple_format build[:commands] %br %b Tag list: @@ -28,6 +28,11 @@ %br %b Refs except: = build[:except] && build[:except].join(", ") + %br + %b When: + = build[:when] + - if build[:allow_failure] + %b Allowed to fail -else %p diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 3a8172dc8e..e3d8d73491 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -155,7 +155,7 @@ - if @builds.present? .build-widget - %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %h4.title #{pluralize(@builds.count(:id), "other build")} for #{@build.short_sha}: %table.table.builds - @builds.each_with_index do |build, i| %tr.build From 3adfee1c8724d56e051da21e18d83435e8b6ba31 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 11:41:22 +0200 Subject: [PATCH 258/443] Allow developer to manage builds --- app/controllers/ci/application_controller.rb | 8 -------- app/controllers/projects/builds_controller.rb | 8 +++++++- app/controllers/projects/commit_controller.rb | 11 ++++++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index 9be470660e..848f2b4e31 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -8,14 +8,6 @@ module Ci private - def authenticate_public_page! - unless project.public - authenticate_user! - - return access_denied! unless can?(current_user, :read_project, gl_project) - end - end - def authenticate_token! unless project.valid_token?(params[:token]) return head(403) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index ad0adc1786..7d72e0b951 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -2,7 +2,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :ci_project before_action :build, except: [:index, :cancel_all] - before_action :authorize_admin_project!, except: [:index, :show, :status] + before_action :authorize_manage_builds!, except: [:index, :show, :status] layout "project" @@ -74,4 +74,10 @@ class Projects::BuildsController < Projects::ApplicationController def build_path(build) namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) end + + def authorize_manage_builds! + unless can?(current_user, :manage_builds, project) + return page_404 + end + end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 7886f3c6de..878c3a66e7 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -4,7 +4,8 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code! + before_action :authorize_download_code!, except: [:cancel_builds] + before_action :authorize_manage_builds!, only: [:cancel_builds] before_action :commit def show @@ -55,4 +56,12 @@ class Projects::CommitController < Projects::ApplicationController def commit @commit ||= @project.commit(params[:id]) end + + private + + def authorize_manage_builds! + unless can?(current_user, :manage_builds, project) + return page_404 + end + end end From f230b42f8442f1e8d29bec7c5f84f73e02c9c845 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 11:41:50 +0200 Subject: [PATCH 259/443] On CI Admin page show only projects that are present in GitLab --- app/models/ci/project.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index eb65c77357..4e806ca1a6 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -99,6 +99,7 @@ module Ci def ordered_by_last_commit_date last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)" joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id"). + joins(:gl_project). order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") end end From 2afb2d3c6788d14039c64dcc2b1ee290c48a0de4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 12:41:17 +0200 Subject: [PATCH 260/443] Fix broken Runners admin page --- .../ci/admin/runners_controller.rb | 1 + app/views/ci/admin/runners/show.html.haml | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb index 110954a612..0cafad2741 100644 --- a/app/controllers/ci/admin/runners_controller.rb +++ b/app/controllers/ci/admin/runners_controller.rb @@ -17,6 +17,7 @@ module Ci @projects = @projects.where(gitlab_id: @gl_projects.select(:id)) end @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? + @projects = @projects.joins(:gl_project) @projects = @projects.page(params[:page]).per(30) end diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 92787b2e6a..02b5361266 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -53,13 +53,14 @@ %th - @runner.runner_projects.each do |runner_project| - project = runner_project.project - %tr.alert-info - %td - %strong - = project.name - %td - .pull-right - = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + - if project.gl_project + %tr.alert-info + %td + %strong + = project.name + %td + .pull-right + = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' %table.table %thead @@ -103,21 +104,26 @@ %th Finished at - @builds.each do |build| + - gl_project = build.gl_project %tr.build %td.id - - gl_project = build.project.gl_project - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do + - if gl_project + = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do + = build.id + - else = build.id %td.status = ci_status_with_icon(build.status) %td.status - = build.project.name + - if gl_project + = gl_project.name_with_namespace %td.build-link - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} + - if gl_project + = link_to ci_status_path(build.commit) do + %strong #{build.commit.short_sha} %td.timestamp - if build.finished_at From 03ea0c74af8bfe6600eb4a7cc04a5d6cfc677f59 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 13:13:22 +0200 Subject: [PATCH 261/443] Use git follow flag for commits page when retrieve history for file or directory Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/models/repository.rb | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c70cc33cce..19a5876c4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.2.0 (unreleased) - Allow users to select the Files view as default project view (Cristian Bica) - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork + - Use git follow flag for commits page when retrieve history for file or directory v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index 88d3d73a40..a517c7d698 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -67,13 +67,16 @@ class Repository end def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) - commits = Gitlab::Git::Commit.where( + options = { repo: raw_repository, ref: ref, path: path, limit: limit, offset: offset, - ) + } + + options[:follow] = true if path.present? + commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? commits end From 0b25d837b8b97010a5ea65002ca09f1aa7f3e504 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 13:33:44 +0200 Subject: [PATCH 262/443] Fail builds that also have empty string --- db/migrate/20151023112551_fail_build_with_empty_name.rb | 5 +++++ db/schema.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151023112551_fail_build_with_empty_name.rb diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb new file mode 100644 index 0000000000..f069bc60ac --- /dev/null +++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb @@ -0,0 +1,5 @@ +class FailBuildWithEmptyName < ActiveRecord::Migration + def change + execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'") + end +end diff --git a/db/schema.rb b/db/schema.rb index 0fec00ebf8..1551956c8b 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: 20151020173906) do +ActiveRecord::Schema.define(version: 20151023112551) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 5921a1edf3eacc23ed25f39d052ad4db4aac91e2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 13:37:37 +0200 Subject: [PATCH 263/443] Fixed nesting for Allowed to fail --- app/views/ci/lints/_create.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index 809fc52dbd..77f78caa8d 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -32,7 +32,7 @@ %b When: = build[:when] - if build[:allow_failure] - %b Allowed to fail + %b Allowed to fail -else %p From 9f2e1f504df2fa9ef7d658cbc73bbdba6f897eda Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 14:39:27 +0200 Subject: [PATCH 264/443] Refactor git follow option set Signed-off-by: Dmitriy Zaporozhets --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index a517c7d698..9d68d8a6df 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -73,9 +73,9 @@ class Repository path: path, limit: limit, offset: offset, + follow: path.present? } - options[:follow] = true if path.present? commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? commits From 201641594d66b04d811f99e106466c45ba7fc1e6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 18:09:17 -0400 Subject: [PATCH 265/443] Vendor clipboard.js --- vendor/assets/javascripts/clipboard.js | 621 +++++++++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 vendor/assets/javascripts/clipboard.js diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js new file mode 100644 index 0000000000..1b1f4f0bd6 --- /dev/null +++ b/vendor/assets/javascripts/clipboard.js @@ -0,0 +1,621 @@ +/*! + * clipboard.js v1.4.2 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Wed, 30 Sep 2015 18:09:45 -0400 Subject: [PATCH 266/443] Add copy_to_clipboard JS --- .../javascripts/copy_to_clipboard.js.coffee | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/assets/javascripts/copy_to_clipboard.js.coffee diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee new file mode 100644 index 0000000000..ec4b80cca6 --- /dev/null +++ b/app/assets/javascripts/copy_to_clipboard.js.coffee @@ -0,0 +1,21 @@ +#= require clipboard + +$ -> + clipboard = new Clipboard '.js-clipboard-trigger', + text: (trigger) -> + $target = $(trigger.nextElementSibling || trigger.previousElementSibling) + $target.data('clipboard-text') || $target.text().trim() + + clipboard.on 'success', (e) -> + $(e.trigger). + tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!'). + tooltip('show') + + # Clear the selection and blur the trigger so it loses its border + e.clearSelection() + $(e.trigger).blur() + + # Manually hide the tooltip after 1 second + setTimeout(-> + $(e.trigger).tooltip('hide') + , 1000) From 831deeeac3b0de16f65ae67f5c0a249a95ba3079 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 23 Oct 2015 15:29:53 +0200 Subject: [PATCH 267/443] Add styling for cross-project-reference clipboard button --- app/assets/stylesheets/pages/issuable.scss | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c60aa5c7fe..abc27a19e3 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -88,3 +88,16 @@ padding: 13px 0; } } + +.cross-project-reference { + text-align: center; + width: 100%; + + .slead { + padding: 5px; + } + + span, button { + background-color: $background-color; + } +} From 187625831f7b501ff6689707d1859b9c7ec4f5e0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 23 Oct 2015 15:31:43 +0200 Subject: [PATCH 268/443] Add "Copy to clipboard" buttons Adds buttons to the commit list SHAs and the cross-project reference data on issuables. --- app/helpers/clipboard_helper.rb | 8 ++++++++ app/views/projects/commits/_commit.html.haml | 3 ++- app/views/projects/issues/_discussion.html.haml | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 app/helpers/clipboard_helper.rb diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb new file mode 100644 index 0000000000..9659507bf0 --- /dev/null +++ b/app/helpers/clipboard_helper.rb @@ -0,0 +1,8 @@ +module ClipboardHelper + def clipboard_button(target = nil) + content_tag :button, + icon('clipboard'), + class: 'btn btn-xs btn-clipboard js-clipboard-trigger', + type: :button + end +end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index cddd5aa3a8..e80cc194d2 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -21,7 +21,8 @@ = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do = ci_status_icon(ci_commit)   - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" + = clipboard_button + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id", data: {clipboard_text: commit.id} .notes_count - if note_count > 0 diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index d4a98eca47..c5fd863ae9 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -17,8 +17,10 @@ - @participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) .col-md-3 - %span.slead.has_tooltip{title: 'Cross-project reference'} - = cross_project_reference(@project, @issue) + .input-group.cross-project-reference + %span.slead.has_tooltip{title: 'Cross-project reference'} + = cross_project_reference(@project, @issue) + = clipboard_button .row %section.col-md-9 From 8010872579dbb28b8f075059c3267356ca793b99 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 23 Oct 2015 15:44:39 +0200 Subject: [PATCH 269/443] Add clipboard buttons to each step of "How to merge" --- app/assets/stylesheets/pages/merge_requests.scss | 9 +++++++++ .../projects/merge_requests/show/_how_to_merge.html.haml | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a1a5208c59..f0b3667acc 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -205,6 +205,15 @@ #modal_merge_info .modal-dialog { width: 600px; + + .btn-clipboard { + @extend .pull-right; + + margin-right: 18px; + margin-top: 5px; + position: absolute; + right: 0; + } } .mr-source-target { diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index f18cf96c17..98f0357ce4 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -3,11 +3,12 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 Check out, review and merge locally + %h3 Check out, review, and merge locally .modal-body %p - %strong Step 1. + %strong Step 1. Fetch and check out the branch for this merge request + = clipboard_button %pre.dark - if @merge_request.for_fork? :preserve @@ -24,6 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up + = clipboard_button %pre.dark - if @merge_request.for_fork? :preserve @@ -36,6 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab + = clipboard_button %pre.dark :preserve git push origin #{h @merge_request.target_branch} From 03cfda94225ad3f6d76d39b52e89f714dde48ecd Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 23 Oct 2015 15:59:25 +0200 Subject: [PATCH 270/443] Remove unused argument from clipboard_button helper --- app/helpers/clipboard_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb index 9659507bf0..3c1d7569fa 100644 --- a/app/helpers/clipboard_helper.rb +++ b/app/helpers/clipboard_helper.rb @@ -1,5 +1,5 @@ module ClipboardHelper - def clipboard_button(target = nil) + def clipboard_button content_tag :button, icon('clipboard'), class: 'btn btn-xs btn-clipboard js-clipboard-trigger', From fe6ec80e75d35b8ead20e1cf8f797cc3ffee3f9b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 17:01:10 +0200 Subject: [PATCH 271/443] Render CI status on merge requests index page Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/models/merge_request.rb | 10 ++++++++-- .../projects/merge_requests/_merge_request.html.haml | 4 ++++ .../projects/merge_requests/widget/_heading.html.haml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2c4afc00ab..53da0148ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.2.0 (unreleased) - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork - Use git follow flag for commits page when retrieve history for file or directory + - Show merge request CI status on merge requests index page v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8d9ad44681..85f37e49e6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -159,11 +159,11 @@ class MergeRequest < ActiveRecord::Base def last_commit merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end + end def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first - end + end def last_commit_short_sha last_commit.short_id @@ -470,4 +470,10 @@ class MergeRequest < ActiveRecord::Base unlock_mr if locked? end end + + def ci_commit + if last_commit + source_project.ci_commit(last_commit.id) + end + end end diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 25e4e8ba80..f9409be5cf 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,3 +1,4 @@ +- ci_commit = merge_request.ci_commit %li{ class: mr_css_classes(merge_request) } .merge-request-title %span.merge-request-title-text @@ -6,6 +7,9 @@ - merge_request.labels.each do |label| = link_to_label(label, project: merge_request.project) .pull-right.light + - if ci_commit + = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do + = ci_status_icon(ci_commit) - if merge_request.merged? %span %i.fa.fa-check diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 10efb81193..a3551516bf 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,4 +1,4 @@ -- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha) +- ci_commit = @merge_request.ci_commit - if ci_commit - status = ci_commit.status .mr-widget-heading From 7b06c83cf72d24edab7adcc00f4000830539506e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 17:01:25 +0200 Subject: [PATCH 272/443] Add tests for MR index page Signed-off-by: Dmitriy Zaporozhets --- .../merge_requests/_merge_request.html.haml | 3 ++- features/project/merge_requests.feature | 6 ++++++ features/steps/project/merge_requests.rb | 13 +++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index f9409be5cf..a285d5d660 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -8,7 +8,8 @@ = link_to_label(label, project: merge_request.project) .pull-right.light - if ci_commit - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do + = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", + title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do = ci_status_icon(ci_commit) - if merge_request.merged? %span diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 83055188ba..f423c3ba54 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -10,6 +10,12 @@ Feature: Project Merge Requests Then I should see "Bug NS-04" in merge requests And I should not see "Feature NS-03" in merge requests + Scenario: I should see CI status for merge requests + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + Given "Bug NS-05" has CI status + When I visit project "Shop" merge requests page + Then I should see merge request "Bug NS-05" with CI status + Scenario: I should see rejected merge requests Given I click link "Closed" Then I should see "Feature NS-03" in merge requests diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 875bf6c467..da34ed758c 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -338,6 +338,19 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(page).to have_content('diff --git') end + step '"Bug NS-05" has CI status' do + project = merge_request.source_project + project.enable_ci + ci_commit = create :ci_commit, gl_project: project, sha: merge_request.last_commit.id + create :ci_build, commit: ci_commit + end + + step 'I should see merge request "Bug NS-05" with CI status' do + page.within ".mr-list" do + expect(page).to have_link "Build status: pending" + end + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end From 696a7084010315078c38507cccabb236bb3d794a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 17:32:52 +0200 Subject: [PATCH 273/443] Move rendering CI status to helper Signed-off-by: Dmitriy Zaporozhets --- app/helpers/ci_status_helper.rb | 7 +++++++ app/views/projects/commits/_commit.html.haml | 3 +-- app/views/projects/merge_requests/_merge_request.html.haml | 4 +--- app/views/shared/projects/_project.html.haml | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index dbd1e26fa7..ceafe5e8c9 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -42,4 +42,11 @@ module CiStatusHelper icon(icon_name) end + + def render_ci_status(ci_commit) + link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", + title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do + ci_status_icon(ci_commit) + end + end end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index cddd5aa3a8..b7998dd12c 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -18,8 +18,7 @@ .pull-right - if ci_commit - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do - = ci_status_icon(ci_commit) + = render_ci_status(ci_commit)   = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index a285d5d660..300a371529 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -8,9 +8,7 @@ = link_to_label(label, project: merge_request.project) .pull-right.light - if ci_commit - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do - = ci_status_icon(ci_commit) + = render_ci_status(ci_commit) - if merge_request.merged? %span %i.fa.fa-check diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index aee839b44e..c36995b94d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -21,9 +21,7 @@ .project-controls - if ci && !project.empty_repo? && project.commit - if ci_commit = project.ci_commit(project.commit.sha) - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do - = ci_status_icon(ci_commit) + = render_ci_status(ci_commit)   - if stars %span From 122f02bc39c690b7fef69f57075380ca82c25b46 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 23 Oct 2015 16:51:44 +0200 Subject: [PATCH 274/443] Remove the contents of the satellites dir --- config/gitlab.yml.example | 6 ++++-- config/initializers/1_settings.rb | 4 +++- db/migrate/20151023144219_remove_satellites.rb | 17 +++++++++++++++++ db/schema.rb | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20151023144219_remove_satellites.rb diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8b85981497..d3aef44705 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -318,10 +318,12 @@ production: &base # ========================== # GitLab Satellites + # + # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at + # least. This setting is fed to 'rm -rf' in + # db/migrate/20151023144219_remove_satellites.rb satellites: - # Relative paths are relative to Rails.root (default: tmp/repo_satellites/) path: /home/git/gitlab-satellites/ - timeout: 30 ## Backup settings backup: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d5493ca038..65e9b0dcb5 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -242,9 +242,11 @@ Settings.git['max_size'] ||= 20971520 # 20.megabytes Settings.git['bin_path'] ||= '/usr/bin/git' Settings.git['timeout'] ||= 10 +# Important: keep the satellites.path setting until GitLab 9.0 at +# least. This setting is fed to 'rm -rf' in +# db/migrate/20151023144219_remove_satellites.rb Settings['satellites'] ||= Settingslogic.new({}) Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root) -Settings.satellites['timeout'] ||= 30 # # Extra customization diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb new file mode 100644 index 0000000000..e73f300028 --- /dev/null +++ b/db/migrate/20151023144219_remove_satellites.rb @@ -0,0 +1,17 @@ +require 'fileutils' + +class RemoveSatellites < ActiveRecord::Migration + def up + satellites = Gitlab.config['satellites'] + return if satellites.nil? + + satellites_path = satellites['path'] + return if satellites_path.nil? + + FileUtils.rm_rf(satellites_path) + end + + def down + # Do nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index 1551956c8b..0fe113325f 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: 20151023112551) do +ActiveRecord::Schema.define(version: 20151023144219) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 5615f2737cb034d3d8de70e1e50f1ba8ca78c34d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Oct 2015 18:21:43 +0200 Subject: [PATCH 275/443] Fix rubocop issues Signed-off-by: Dmitriy Zaporozhets --- app/helpers/ci_status_helper.rb | 6 ++++-- features/steps/project/merge_requests.rb | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index ceafe5e8c9..ed88df5dd8 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -44,8 +44,10 @@ module CiStatusHelper end def render_ci_status(ci_commit) - link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do + link_to ci_status_path(ci_commit), + class: "c#{ci_status_color(ci_commit)}", + title: "Build status: #{ci_commit.status}", + data: { toggle: 'tooltip', placement: 'left' } do ci_status_icon(ci_commit) end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index da34ed758c..92ec14d0d7 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -345,11 +345,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps create :ci_build, commit: ci_commit end - step 'I should see merge request "Bug NS-05" with CI status' do - page.within ".mr-list" do - expect(page).to have_link "Build status: pending" - end - end + step 'I should see merge request "Bug NS-05" with CI status' do + page.within ".mr-list" do + expect(page).to have_link "Build status: pending" + end + end def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") From b2663d1ca171c62435c292481ff5b45eb4230f4b Mon Sep 17 00:00:00 2001 From: Derek Robati Date: Fri, 23 Oct 2015 12:48:17 -0400 Subject: [PATCH 276/443] Added missing period. --- 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 f608674faf..9a24a1e252 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](http://scottchacon.com/2011/08/31/github-flow.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 7e441bbb4023ed7531c23b2d2e5f063ccc820c40 Mon Sep 17 00:00:00 2001 From: Jannick Fahlbusch Date: Fri, 23 Oct 2015 23:37:28 +0200 Subject: [PATCH 277/443] Add missing character This adds the missing character `a` to the help-text --- app/views/projects/ci_settings/_no_runners.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci_settings/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml index 33038c5297..1374e6680f 100644 --- a/app/views/projects/ci_settings/_no_runners.html.haml +++ b/app/views/projects/ci_settings/_no_runners.html.haml @@ -5,4 +5,4 @@ You can add Specific runner for this project on Runners page - if current_user.admin - or add Shared runner for whole application in admin are. + or add Shared runner for whole application in admin area. From 45399ccc9a90e575ca9b98d2acf7a6a8c526af68 Mon Sep 17 00:00:00 2001 From: Jannick Fahlbusch Date: Sat, 24 Oct 2015 12:37:44 +0200 Subject: [PATCH 278/443] Fix some grammatical issues --- app/views/ci/admin/runner_projects/index.html.haml | 2 +- app/views/ci/admin/runners/index.html.haml | 4 ++-- app/views/ci/admin/runners/show.html.haml | 4 ++-- app/views/ci/user_sessions/new.html.haml | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml index f049b4f4c4..6b4e3b2cb3 100644 --- a/app/views/ci/admin/runner_projects/index.html.haml +++ b/app/views/ci/admin/runner_projects/index.html.haml @@ -1,5 +1,5 @@ %p.lead - To register new runner visit #{link_to 'this page ', ci_runners_path} + To register a new runner visit #{link_to 'this page ', ci_runners_path} .row .col-md-8 diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index bb213fbffc..bacaccfbff 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -1,5 +1,5 @@ %p.lead - %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. + %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. %code #{GitlabCi::REGISTRATION_TOKEN} .bs-callout @@ -21,7 +21,7 @@ \- run builds from assigned projects %li %span.label.label-danger paused - \- runner will not receive any new build + \- runner will not receive any new builds .append-bottom-20.clearfix .pull-left diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 02b5361266..1498db46a8 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -13,13 +13,13 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This runner will process build from ALL UNASSIGNED projects + %h4 This runner will process builds from ALL UNASSIGNED projects %p If you want runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This runner will process build only from ASSIGNED projects + %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr = form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml index 308b217ea7..b8d9a1d708 100644 --- a/app/views/ci/user_sessions/new.html.haml +++ b/app/views/ci/user_sessions/new.html.haml @@ -1,8 +1,7 @@ .login-block %h2 Login using GitLab account %p.light - Make sure you have account on GitLab server + Make sure you have an account on the GitLab server = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink %hr = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) - From ab72c12028072e9508ee730e39a68974b2a714f2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 24 Oct 2015 15:58:43 +0200 Subject: [PATCH 279/443] Remove "Install schedules" step from Installation guide [ci skip] --- doc/install/installation.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 2e9ac7393e..36d6ec79fd 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -346,11 +346,6 @@ The `secrets.yml` file stores encryption keys for sessions and secure variables. Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups. Otherwise your secrets are exposed if one of your backups is compromised. -### Install schedules - - # Setup schedules - sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production - ### Install Init Script Download the init script (will be `/etc/init.d/gitlab`): From c3d48f97355371d6c8760e05637f666f23c2a76a Mon Sep 17 00:00:00 2001 From: kazubu Date: Mon, 26 Oct 2015 00:02:32 +0900 Subject: [PATCH 280/443] Fix: 500 error returned if destroy request without HTTP referer --- CHANGELOG | 1 + app/controllers/projects_controller.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 53da0148ff..2b69b38360 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.2.0 (unreleased) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page + - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 82119022cf..743c429b72 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -124,7 +124,7 @@ class ProjectsController < ApplicationController ::Projects::DestroyService.new(@project, current_user, {}).execute flash[:alert] = "Project '#{@project.name}' was deleted." - if request.referer.include?('/admin') + if request.referer.present? && request.referer.include?('/admin') redirect_to admin_namespaces_projects_path else redirect_to dashboard_projects_path From 0bfb9cbf38c72f801255b910430fdbff6536b73d Mon Sep 17 00:00:00 2001 From: kazubu Date: Mon, 26 Oct 2015 14:58:09 +0900 Subject: [PATCH 281/443] modify to use redirect_back_or_default function --- app/controllers/projects_controller.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 743c429b72..05c7d3de8b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -124,11 +124,7 @@ class ProjectsController < ApplicationController ::Projects::DestroyService.new(@project, current_user, {}).execute flash[:alert] = "Project '#{@project.name}' was deleted." - if request.referer.present? && request.referer.include?('/admin') - redirect_to admin_namespaces_projects_path - else - redirect_to dashboard_projects_path - end + redirect_back_or_default(default: dashboard_projects_path, options: {}) rescue Projects::DestroyService::DestroyError => ex redirect_to edit_project_path(@project), alert: ex.message end From eb3c4255bf1926a1edf02c2a4792feaaf6fe4091 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 26 Oct 2015 10:14:35 +0100 Subject: [PATCH 282/443] Bump nprogress-rails to 0.1.6.7 Closes #2866 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 9254ce2ccf..f72ca7aa53 100644 --- a/Gemfile +++ b/Gemfile @@ -201,7 +201,7 @@ gem 'jquery-atwho-rails', '~> 1.0.0' gem 'jquery-rails', '~> 3.1.3' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 4.2.1' -gem 'nprogress-rails', '~> 0.1.2.3' +gem 'nprogress-rails', '~> 0.1.6.7' gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.2.0' gem 'select2-rails', '~> 3.5.9' diff --git a/Gemfile.lock b/Gemfile.lock index 53122898b0..7016c59e0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -406,7 +406,7 @@ GEM newrelic_rpm (3.9.4.245) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - nprogress-rails (0.1.2.3) + nprogress-rails (0.1.6.7) oauth (0.4.7) oauth2 (1.0.0) faraday (>= 0.8, < 0.10) @@ -854,7 +854,7 @@ DEPENDENCIES nested_form (~> 0.3.2) newrelic-grape newrelic_rpm (~> 3.9.4.245) - nprogress-rails (~> 0.1.2.3) + nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) From 63c74bb0eee81b96ce675d5875f2bbad04362353 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 26 Oct 2015 10:39:33 +0100 Subject: [PATCH 283/443] Bump jquery-atwho-rails to ~> 1.3.2 Closes #2858 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 9254ce2ccf..39ec04951c 100644 --- a/Gemfile +++ b/Gemfile @@ -197,7 +197,7 @@ gem 'bootstrap-sass', '~> 3.0' gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.1' gem 'gon', '~> 5.0.0' -gem 'jquery-atwho-rails', '~> 1.0.0' +gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 3.1.3' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 4.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 53122898b0..340eb0fc30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -354,7 +354,7 @@ GEM ice_nine (0.11.1) inflecto (0.0.2) ipaddress (0.8.0) - jquery-atwho-rails (1.0.1) + jquery-atwho-rails (1.3.2) jquery-rails (3.1.3) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) @@ -840,7 +840,7 @@ DEPENDENCIES hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) - jquery-atwho-rails (~> 1.0.0) + jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 3.1.3) jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.0.1) From 6db014987d3c9cd4595adad70bb8a11ccacf9545 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 23 Oct 2015 18:15:13 +0200 Subject: [PATCH 284/443] Fix specific runner visibility --- CHANGELOG | 1 + .../projects/runners_controller.rb | 9 ++++---- app/models/ci/runner.rb | 1 + app/models/user.rb | 23 +++++++++++-------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 53da0148ff..215a8d2774 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.1.0 - Speed up load times of issue detail pages by roughly 1.5x - Require CI jobs to be named - Fix CI rendering regressions + - Fix specific runners visibility - Allow developer to manage builds - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index deb07a2141..bfbcf2567f 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -6,11 +6,10 @@ class Projects::RunnersController < Projects::ApplicationController layout 'project_settings' def index - @runners = @ci_project.runners.order('id DESC') - @specific_runners = - Ci::Runner.specific.includes(:runner_projects). - where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ). - where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20) + @runners = @ci_project.runners.ordered + @specific_runners = current_user.ci_authorized_runners. + where.not(id: @ci_project.runners). + ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 1b3669f1b7..b719ad3c87 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -36,6 +36,7 @@ module Ci scope :active, ->() { where(active: true) } scope :paused, ->() { where(active: false) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } + scope :ordered, ->() { order(id: :desc) } acts_as_taggable diff --git a/app/models/user.rb b/app/models/user.rb index 7e4321d537..c72beacbf0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -401,15 +401,17 @@ class User < ActiveRecord::Base end end + def authorized_projects_id + @authorized_projects_id ||= begin + project_ids = personal_projects.pluck(:id) + project_ids.push(*groups_projects.pluck(:id)) + project_ids.push(*projects.pluck(:id).uniq) + end + end # Projects user has access to def authorized_projects - @authorized_projects ||= begin - project_ids = personal_projects.pluck(:id) - project_ids.push(*groups_projects.pluck(:id)) - project_ids.push(*projects.pluck(:id).uniq) - Project.where(id: project_ids) - end + @authorized_projects ||= Project.where(id: authorized_projects_id) end def owned_projects @@ -768,11 +770,14 @@ class User < ActiveRecord::Base end def ci_authorized_projects - @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects) + @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects_id) end def ci_authorized_runners - Ci::Runner.specific.includes(:runner_projects). - where(ci_runner_projects: { project_id: ci_authorized_projects } ) + @ci_authorized_runners ||= begin + runner_ids = Ci::RunnerProject.joins(:project). + where(ci_projects: { gitlab_id: authorized_projects_id }).select(:runner_id) + Ci::Runner.specific.where(id: runner_ids) + end end end From 271ad4e354d10c7ff59f57a1852c5a97e3621273 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 26 Oct 2015 12:23:34 +0100 Subject: [PATCH 285/443] Fix CI badge The previous code relied on having on ref stored in commit, however the ref was moved to the build. --- CHANGELOG | 1 + app/models/ci/project_status.rb | 4 ---- app/services/ci/image_for_build_service.rb | 16 +++++++--------- spec/services/ci/image_for_build_service_spec.rb | 5 +++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 53da0148ff..b0b80f4abf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.1.0 - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x + - Fix CI badge - Require CI jobs to be named - Fix CI rendering regressions - Allow developer to manage builds diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb index b66f1212f2..2d35aeac22 100644 --- a/app/models/ci/project_status.rb +++ b/app/models/ci/project_status.rb @@ -27,9 +27,5 @@ module Ci def human_status status end - - def last_commit_for_ref(ref) - commits.where(ref: ref).last - end end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index b95835ba09..b8d2419303 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,17 +1,15 @@ module Ci class ImageForBuildService def execute(project, params) - image_name = - if params[:sha] - commit = project.commits.find_by(sha: params[:sha]) - image_for_commit(commit) - elsif params[:ref] - commit = project.last_commit_for_ref(params[:ref]) - image_for_commit(commit) - else - 'build-unknown.svg' + sha = params[:sha] + sha ||= + if params[:ref] + project.gl_project.commit(params[:ref]).try(:sha) end + commit = project.commits.ordered.find_by(sha: sha) + image_name = image_for_commit(commit) + image_path = Rails.root.join('public/ci', image_name) OpenStruct.new( diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index d7242d684c..cda7d0c4a5 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -4,8 +4,9 @@ module Ci describe ImageForBuildService do let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:ci_project) } - let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } - let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') } + let(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) } + let(:commit_sha) { gl_project.commit('master').sha } + let(:commit) { gl_project.ensure_ci_commit(commit_sha) } let(:build) { FactoryGirl.create(:ci_build, commit: commit) } describe :execute do From 0b082b348b517af854265eaff530b2b6d3cfd7b2 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 26 Oct 2015 13:42:09 +0100 Subject: [PATCH 286/443] Start putting shared files in "shared" --- CONTRIBUTING.md | 1 + config/gitlab.yml.example | 4 ++++ config/initializers/1_settings.rb | 6 +++++- doc/development/shared_files.md | 33 +++++++++++++++++++++++++++++++ shared/.gitkeep | 0 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 doc/development/shared_files.md create mode 100644 shared/.gitkeep diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69abadb151..9f79ff413a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,6 +83,7 @@ If you can, please submit a merge request with the fix or improvements including 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). 1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. +1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md). The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases. diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8b85981497..ad0f76e8d0 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -310,6 +310,10 @@ production: &base # application_name: 'YOUR_APP_NAME', # application_password: 'YOUR_APP_PASSWORD' } } + # Shared file storage settings + shared: + # path: /mnt/gitlab # Default: shared + diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d5493ca038..75b6eadf61 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -125,6 +125,9 @@ Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link Settings.omniauth['providers'] ||= [] +Settings['shared'] ||= Settingslogic.new({}) +Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) + Settings['issues_tracker'] ||= {} # @@ -169,7 +172,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) -Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) +Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -246,6 +249,7 @@ Settings['satellites'] ||= Settingslogic.new({}) Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root) Settings.satellites['timeout'] ||= 30 + # # Extra customization # diff --git a/doc/development/shared_files.md b/doc/development/shared_files.md new file mode 100644 index 0000000000..cffe6f7919 --- /dev/null +++ b/doc/development/shared_files.md @@ -0,0 +1,33 @@ +# Shared files + +Historically, GitLab has been storing shared files in many different +directories: `public/uploads`, `builds`, `tmp/repositories`, `tmp/rebase` (EE), +etc. Having so many shared directories makes it difficult to deploy GitLab on +shared storage (e.g. NFS). Working towards GitLab 9.0 we are consolidating +these different directories under the `shared` directory. + +This means that if GitLab will start storing puppies in some future version +then we should put them in `shared/puppies`. Temporary files should be stored +in `shared/tmp`. + +In the GitLab application code you can get the full path to the `shared` +directory with `Gitlab.config.shared.path`. + +## What is not a 'shared file' + +Files that belong to only one process, or on only one server, should not go in +`shared`. Examples include PID files and sockets. + +## Temporary files and shared storage + +Sometimes you create a temporary file on disk with the intention of it becoming +'official'. For example you might be first streaming an upload from a user to +disk in a temporary file so you can perform some checks on it. When the checks +pass, you make the file official. In scenarios like this please follow these +rules: + +- Store the temporary file under `shared/tmp`, i.e. on the same filesystem you + want the official file to be on. +- Use move/rename operations when operating on the file instead of copy + operations where possible, because renaming a file is much faster than + copying it. diff --git a/shared/.gitkeep b/shared/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From 5ff830cf32908c0efc156e439a0486cfb40eeadb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 26 Oct 2015 14:28:31 +0100 Subject: [PATCH 287/443] Clarify puppies --- doc/development/shared_files.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/shared_files.md b/doc/development/shared_files.md index cffe6f7919..fcd905b54a 100644 --- a/doc/development/shared_files.md +++ b/doc/development/shared_files.md @@ -7,8 +7,8 @@ shared storage (e.g. NFS). Working towards GitLab 9.0 we are consolidating these different directories under the `shared` directory. This means that if GitLab will start storing puppies in some future version -then we should put them in `shared/puppies`. Temporary files should be stored -in `shared/tmp`. +then we should put them in `shared/puppies`. Temporary puppy files should be +stored in `shared/tmp`. In the GitLab application code you can get the full path to the `shared` directory with `Gitlab.config.shared.path`. From 2f56d89c9e22637cdb9e310c41f724f9ed450ea5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 26 Oct 2015 16:19:07 +0100 Subject: [PATCH 288/443] Update CHANGELOG for 8.1.1 [ci skip] --- CHANGELOG | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 24aaa449e6..256cb58fd3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - - Fix cloning Wiki repositories via HTTP (Stan Hu) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL @@ -13,11 +12,20 @@ v 8.2.0 (unreleased) - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page +v 8.1.1 + - Fix cloning Wiki repositories via HTTP (Stan Hu) + - Add migration to remove satellites directory + - Fix specific runners visibility + - Fix 500 when editing CI service + - Require CI jobs to be named + - Fix CSS for runner status + - Fix CI badge + - Allow developer to manage builds + v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - - Fix CSS for runner status - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. @@ -25,16 +33,11 @@ v 8.1.0 - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu) - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu) - Speed up load times of issue detail pages by roughly 1.5x - - Fix CI badge - - Require CI jobs to be named - Fix CI rendering regressions - - Fix specific runners visibility - - Allow developer to manage builds - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg) - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu) - Make diff file view easier to use on mobile screens (Stan Hu) - Improved performance of finding users by username or Email address - - Fix 500 when editing CI service - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu) - Add support for creating directories from Files page (Stan Hu) - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) From 01c7769c7529ca6d4036220052d78fc7284262f7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 26 Oct 2015 19:31:15 +0100 Subject: [PATCH 289/443] Add projects path index Fixes performance regression introduced by MR1649 --- db/migrate/20151026182941_add_project_path_index.rb | 9 +++++++++ db/schema.rb | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20151026182941_add_project_path_index.rb diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb new file mode 100644 index 0000000000..a62fe199d7 --- /dev/null +++ b/db/migrate/20151026182941_add_project_path_index.rb @@ -0,0 +1,9 @@ +class AddProjectPathIndex < ActiveRecord::Migration + def up + add_index :projects, :path + end + + def down + remove_index :projects, :path + end +end diff --git a/db/schema.rb b/db/schema.rb index 0fe113325f..4bde9f0b74 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: 20151023144219) do +ActiveRecord::Schema.define(version: 20151026182941) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -624,6 +624,7 @@ ActiveRecord::Schema.define(version: 20151023144219) do add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree + add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree create_table "protected_branches", force: true do |t| From c9af886df9b83e7f3f9b131f19184546fbeac9de Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Oct 2015 12:33:54 +0100 Subject: [PATCH 290/443] Remove deprecated CI events from project settings page Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/events_controller.rb | 21 ------------------ .../ci/runner_projects_controller.rb | 2 -- app/views/ci/events/index.html.haml | 20 ----------------- app/views/layouts/ci/project.html.haml | 11 ---------- .../layouts/nav/_project_settings.html.haml | 5 ----- config/routes.rb | 2 -- spec/features/ci/events_spec.rb | 22 ------------------- 8 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 app/controllers/ci/events_controller.rb delete mode 100644 app/views/ci/events/index.html.haml delete mode 100644 app/views/layouts/ci/project.html.haml delete mode 100644 spec/features/ci/events_spec.rb diff --git a/CHANGELOG b/CHANGELOG index ea8c6fb5c1..b8c23cbef4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.2.0 (unreleased) - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) + - Remove deprecated CI events from project settings page v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb deleted file mode 100644 index 89b784a1e8..0000000000 --- a/app/controllers/ci/events_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Ci - class EventsController < Ci::ApplicationController - EVENTS_PER_PAGE = 50 - - before_action :authenticate_user! - before_action :project - before_action :authorize_manage_project! - - layout 'ci/project' - - def index - @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb index 97f01d40af..9d55531336 100644 --- a/app/controllers/ci/runner_projects_controller.rb +++ b/app/controllers/ci/runner_projects_controller.rb @@ -4,8 +4,6 @@ module Ci before_action :project before_action :authorize_manage_project! - layout 'ci/project' - def create @runner = Ci::Runner.find(params[:runner_project][:runner_id]) diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml deleted file mode 100644 index 9824e85b1a..0000000000 --- a/app/views/ci/events/index.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -%h3.page-title Events - -.table-holder - %table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago - -= paginate @events diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml deleted file mode 100644 index 15478c3f5b..0000000000 --- a/app/views/layouts/ci/project.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title @project.name, ci_project_path(@project) - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 954dbe5d2b..356ce09c3d 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -65,8 +65,3 @@ = icon('share fw') %span CI Services - = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project.gitlab_ci_project) do - = icon('book fw') - %span - CI Events diff --git a/config/routes.rb b/config/routes.rb index f6812c9280..0458f538eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,8 +23,6 @@ Gitlab::Application.routes.draw do end resources :runner_projects, only: [:create, :destroy] - - resources :events, only: [:index] end resource :user_sessions do diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb deleted file mode 100644 index 5b9fd40415..0000000000 --- a/spec/features/ci/events_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe "Events" do - let(:user) { create(:user) } - let(:project) { FactoryGirl.create :ci_project } - let(:event) { FactoryGirl.create :ci_admin_event, project: project } - - before do - login_as(user) - project.gl_project.team << [user, :master] - end - - describe "GET /ci/project/:id/events" do - before do - event - visit ci_project_events_path(project) - end - - it { expect(page).to have_content "Events" } - it { expect(page).to have_content event.description } - end -end From 910c2a50ef59b4233f6d3903f5c0e980b5e9ab7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 28 Oct 2015 12:41:56 +0100 Subject: [PATCH 291/443] Fixed the permission doc The guest users was missing "Pull project code" and "Download project". --- doc/permissions/permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 7a6a195844..8d4c2ceab7 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -15,8 +15,8 @@ documentation](doc/workflow/add-user/add-user.md). |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| Pull project code | | ✓ | ✓ | ✓ | ✓ | -| Download project | | ✓ | ✓ | ✓ | ✓ | +| Pull project code | ✓ | ✓ | ✓ | ✓ | ✓ | +| Download project | ✓ | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | From aaa93d5add41661b657c3a684e23c4084f6292b5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 28 Oct 2015 13:09:51 +0100 Subject: [PATCH 292/443] Use issue editor as cross reference comment author when issue is edited with a new mention. --- CHANGELOG | 1 + app/services/issues/update_service.rb | 2 +- app/services/merge_requests/update_service.rb | 2 +- app/services/notes/update_service.rb | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ea8c6fb5c1..5fe1d957eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.2.0 (unreleased) - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) + - Use issue editor as cross reference comment author when issue is edited with a new mention. v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 2b5426ad45..551325e4ca 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -35,7 +35,7 @@ module Issues create_title_change_note(issue, issue.previous_changes['title'].first) end - issue.create_new_cross_references! + issue.create_new_cross_references!(current_user) execute_hooks(issue, 'update') end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index ebbe0af803..61f7d2bbe8 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -59,7 +59,7 @@ module MergeRequests merge_request.mark_as_unchecked end - merge_request.create_new_cross_references! + merge_request.create_new_cross_references!(current_user) execute_hooks(merge_request, 'update') end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 6c2f08e596..72e2f78008 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -4,7 +4,7 @@ module Notes return note unless note.editable? note.update_attributes(params.merge(updated_by: current_user)) - note.create_new_cross_references! + note.create_new_cross_references!(current_user) note.reset_events_cache note From 89bce7fada0d02caf27b13bc638aaaf5956b9c80 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Mon, 26 Oct 2015 11:34:36 +0100 Subject: [PATCH 293/443] Add copy paste text for closing down the Github issue tracker Signed-off-by: Jeroen van Baarsen --- PROCESS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PROCESS.md b/PROCESS.md index 9f4b708d2b..d42168a723 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -115,3 +115,10 @@ We can only accept a merge request if all the tests are green. I've just restarted the build. When the tests are still not passing after this restart and you're sure that is does not have anything to do with your code changes, please rebase with master to see if that solves the issue. + +### Closing down the issue tracker on GitHub + +We are currently in the process of closing down the issue tracker on GitHub, to +prevent duplication with the [GitLab.com issue tracker][https://gitlab.com/gitlab-org/gitlab-ce/issues]. +Since this is an older issue I'll be closing this for now. If you think this is +still an issue I encourage you to open it on the [GitLab.com issue tracker][https://gitlab.com/gitlab-org/gitlab-ce/issues]. From f56c7d9f8e66c69de6e984e497dd529874b8d638 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 23 Oct 2015 11:13:10 -0700 Subject: [PATCH 294/443] Force update refs/merge-requests/X/head upon a push to the source branch of a merge request Closes #3138 --- CHANGELOG | 1 + app/models/repository.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0d89fca9fc..fa13343ea2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL diff --git a/app/models/repository.rb b/app/models/repository.rb index a3ba5f4c18..c9b36bd817 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -528,7 +528,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(git fetch #{source_path} #{source_ref}:#{target_ref}) + args = %W(git fetch -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end From 5a5069969ce8e9184e36abbb7623bf474d5869e9 Mon Sep 17 00:00:00 2001 From: Jonathan Schoeffling Date: Sun, 14 Jun 2015 18:04:20 -0400 Subject: [PATCH 295/443] Add support for searching commit log messages Include the log messages of recent commits in project-level search results, providing functionality similar to 'git log --grep'. Update repository model rspec tests to validate the output of Repository#commits_with_log_matching. --- CHANGELOG | 1 + app/controllers/search_controller.rb | 4 +-- app/models/repository.rb | 6 ++++ app/views/layouts/_search.html.haml | 2 ++ app/views/search/_category.html.haml | 7 +++++ app/views/search/results/_commits.html.haml | 32 +++++++++++++++++++++ lib/gitlab/project_search_results.rb | 12 +++++++- spec/models/repository_spec.rb | 11 +++++++ 8 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 app/views/search/results/_commits.html.haml diff --git a/CHANGELOG b/CHANGELOG index 0d89fca9fc..3975f236a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -208,6 +208,7 @@ v 8.0.0 - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato) - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato) - Removed API calls from CE to CI + - Include commit logs in project search v 7.14.3 - No changes diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index eb0408a95e..9bb42ec86b 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -23,8 +23,8 @@ class SearchController < ApplicationController @search_results = if @project - unless %w(blobs notes issues merge_requests milestones wiki_blobs). - include?(@scope) + unless %w(blobs notes issues merge_requests milestones wiki_blobs + commits).include?(@scope) @scope = 'blobs' end diff --git a/app/models/repository.rb b/app/models/repository.rb index a3ba5f4c18..39451f7da7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -87,6 +87,12 @@ class Repository commits end + def commits_with_log_matching(query) + list = Gitlab::Git::Commit.where(repo: raw_repository, limit: 1000) + list = Commit.decorate(list, @project) if list.present? + list.select! { |c| c.message.match /#{query}/i } + end + def find_branch(name) branches.find { |branch| branch.name == name } end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index ceb64ce315..d1aa8f6246 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -11,6 +11,8 @@ = hidden_field_tag :scope, 'merge_requests' - elsif current_controller?(:wikis) = hidden_field_tag :scope, 'wiki_blobs' + - elsif current_controller?(:commits) + = hidden_field_tag :scope, 'commits' - else = hidden_field_tag :search_code, true diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index d637abfa76..ef43f727d8 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -42,6 +42,13 @@ Wiki %span.badge = @search_results.wiki_blobs_count + %li{class: ("active" if @scope == 'commits')} + = link_to search_filter_path(scope: 'commits') do + = icon('history fw') + %span + Commit Logs + %span.badge + = @search_results.commits_count - elsif @show_snippets %li{class: ("active" if @scope == 'snippet_blobs')} diff --git a/app/views/search/results/_commits.html.haml b/app/views/search/results/_commits.html.haml new file mode 100644 index 0000000000..8076174e59 --- /dev/null +++ b/app/views/search/results/_commits.html.haml @@ -0,0 +1,32 @@ +.search-result-row + .commits-row-title + %strong.str-truncated + = link_to commits.title, namespace_project_commit_path(@project.namespace, @project, commits.id), class: "commit_short_id" + + .pull-right + = link_to commits.short_id, namespace_project_commit_path(@project.namespace, @project, commits.id), class: "commit_short_id" + + .notes_count + - if @note_counts + - note_count = @note_counts.fetch(commits.id, 0) + - else + - notes = commits.notes + - note_count = notes.user.count + + - if note_count > 0 + %span.light + %i.fa.fa-comments + = note_count + + - if commits.description? + .commits-row-description + %pre + = preserve(gfm(escape_once(commits.description))) + + .commits-row-info + = commit_author_link(commits, avatar: true, size: 24) + authored + .committed_ago + #{time_ago_with_tooltip(commits.committed_date)}   + = link_to_browse_code(@project, commits) + %br diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 0a2be605af..3bf98699bc 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -20,6 +20,8 @@ module Gitlab Kaminari.paginate_array(blobs).page(page).per(per_page) when 'wiki_blobs' Kaminari.paginate_array(wiki_blobs).page(page).per(per_page) + when 'commits' + Kaminari.paginate_array(commits).page(page).per(per_page) else super end @@ -27,7 +29,7 @@ module Gitlab def total_count @total_count ||= issues_count + merge_requests_count + blobs_count + - notes_count + wiki_blobs_count + notes_count + wiki_blobs_count + commits_count end def blobs_count @@ -42,6 +44,10 @@ module Gitlab @wiki_blobs_count ||= wiki_blobs.count end + def commits_count + @commits_count ||= commits.count + end + private def blobs @@ -70,6 +76,10 @@ module Gitlab Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC') end + def commits + project.repository.commits_with_log_matching(query) + end + def limit_project_ids [project.id] end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 05e51532eb..ceffd5a861 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -26,6 +26,17 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end + describe :commits_with_log_matching do + subject { repository.commits_with_log_matching('submodule'). + map{|k| k.id} + } + + it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') } + it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + end + describe :blob_at do context 'blank sha' do subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') } From 091aa95dbfec0b062ed706fb55589ce10e5bd9d0 Mon Sep 17 00:00:00 2001 From: Mike Chmielewski Date: Wed, 21 Oct 2015 00:59:52 +0000 Subject: [PATCH 296/443] Make subject of spec line up on one line to satisfy rubocop requirements --- spec/models/repository_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index ceffd5a861..9d5859f147 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -27,9 +27,7 @@ describe Repository do end describe :commits_with_log_matching do - subject { repository.commits_with_log_matching('submodule'). - map{|k| k.id} - } + subject { repository.commits_with_log_matching('submodule').map{|k| k.id} } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } @@ -102,4 +100,4 @@ describe Repository do it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } end end -end +end \ No newline at end of file From afdb53baecb2ef6d57f8ed957213260825c67805 Mon Sep 17 00:00:00 2001 From: Mike Chmielewski Date: Wed, 21 Oct 2015 01:04:19 +0000 Subject: [PATCH 297/443] Added @commits to list of tags. --- lib/tasks/spinach.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index c8881be095..d5a96fd38f 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -5,7 +5,7 @@ namespace :spinach do task :project do cmds = [ %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets), + %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@commits), ] run_commands(cmds) end @@ -14,7 +14,7 @@ namespace :spinach do task :other do cmds = [ %W(rake gitlab:setup), - %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets), + %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets,@commits), ] run_commands(cmds) end @@ -33,4 +33,4 @@ def run_commands(cmds) cmds.each do |cmd| system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") end -end +end \ No newline at end of file From 8e3a316121ba0814997cc66a8ef00fb97ea0f200 Mon Sep 17 00:00:00 2001 From: Michael Chmielewski Date: Tue, 20 Oct 2015 21:44:39 -0400 Subject: [PATCH 298/443] Moved changelog entry to 8.2 --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3975f236a5..b573d40317 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,9 @@ v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) + - Include commit logs in project search + +v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. @@ -208,7 +211,6 @@ v 8.0.0 - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato) - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato) - Removed API calls from CE to CI - - Include commit logs in project search v 7.14.3 - No changes From 8e8fb87d4091f05a75929d6daa3bbe6862e2dda1 Mon Sep 17 00:00:00 2001 From: Michael Chmielewski Date: Sat, 24 Oct 2015 22:41:51 -0400 Subject: [PATCH 299/443] Trailing new lines at ends of files are important. --- spec/models/repository_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9d5859f147..a6973ea8fc 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -100,4 +100,4 @@ describe Repository do it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } end end -end \ No newline at end of file +end From b1f4aaa5e753e6e7cdefd84226839123df59b382 Mon Sep 17 00:00:00 2001 From: Michael Chmielewski Date: Tue, 27 Oct 2015 21:16:56 -0400 Subject: [PATCH 300/443] Trying to incorporate suggestions from comments on Merge Request 1661 --- app/models/repository.rb | 9 +++++---- lib/gitlab/project_search_results.rb | 2 +- spec/models/repository_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 39451f7da7..ed7ed9fd26 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -87,10 +87,11 @@ class Repository commits end - def commits_with_log_matching(query) - list = Gitlab::Git::Commit.where(repo: raw_repository, limit: 1000) - list = Commit.decorate(list, @project) if list.present? - list.select! { |c| c.message.match /#{query}/i } + def find_commits_with_matching_log(query) + # Limited to 1000 commits for now, could be parameterized? + args = %W(git log --pretty=%H --max-count 1000 --grep=#{query}) + + Gitlab::Popen.popen(args, path_to_repo) end def find_branch(name) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 3bf98699bc..54389f7d66 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -77,7 +77,7 @@ module Gitlab end def commits - project.repository.commits_with_log_matching(query) + project.repository.find_commits_with_matching_log(query) end def limit_project_ids diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a6973ea8fc..26c92af31e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -26,8 +26,8 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end - describe :commits_with_log_matching do - subject { repository.commits_with_log_matching('submodule').map{|k| k.id} } + describe :find_commits_with_matching_log do + subject { repository.find_commits_with_matching_log('submodule').map{|k| k.id} } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } From 7b62791afc8e98f8ccd7d85dbae0cf2128883c13 Mon Sep 17 00:00:00 2001 From: Michael Chmielewski Date: Tue, 27 Oct 2015 23:24:43 -0400 Subject: [PATCH 301/443] Fixed method to use git log via Popen as recommended, and made output match test (and thus system) expectations. --- app/models/repository.rb | 7 ++++++- spec/models/repository_spec.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index ed7ed9fd26..112ad05c18 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -91,7 +91,12 @@ class Repository # Limited to 1000 commits for now, could be parameterized? args = %W(git log --pretty=%H --max-count 1000 --grep=#{query}) - Gitlab::Popen.popen(args, path_to_repo) + git_log_results = Gitlab::Popen.popen(args, path_to_repo) + + # 1. Get result, which is 1-element array + # 2. Split on lines + # 3. Recreate array, but remove trailing newline characters on each element + git_log_results.first.lines.map{ |l| l.chomp } end def find_branch(name) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 26c92af31e..17b5f28891 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -27,7 +27,7 @@ describe Repository do end describe :find_commits_with_matching_log do - subject { repository.find_commits_with_matching_log('submodule').map{|k| k.id} } + subject { repository.find_commits_with_matching_log('submodule') } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } From a0d0a0179134c16c84dfa18da9db78b375cd7cd0 Mon Sep 17 00:00:00 2001 From: Michael Chmielewski Date: Wed, 28 Oct 2015 22:12:22 -0400 Subject: [PATCH 302/443] Actually converted code to following suggestions. --- app/models/repository.rb | 9 +++------ spec/models/repository_spec.rb | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 112ad05c18..a0f2b3fb76 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -91,12 +91,9 @@ class Repository # Limited to 1000 commits for now, could be parameterized? args = %W(git log --pretty=%H --max-count 1000 --grep=#{query}) - git_log_results = Gitlab::Popen.popen(args, path_to_repo) - - # 1. Get result, which is 1-element array - # 2. Split on lines - # 3. Recreate array, but remove trailing newline characters on each element - git_log_results.first.lines.map{ |l| l.chomp } + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map{ |l| l.chomp } + commits = git_log_results.map{ |c| commit(c) } + commits end def find_branch(name) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 17b5f28891..aedbfa04d8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -27,7 +27,7 @@ describe Repository do end describe :find_commits_with_matching_log do - subject { repository.find_commits_with_matching_log('submodule') } + subject { repository.find_commits_with_matching_log('submodule').map{ |k| k.id } } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } From 7794cc8b9d1e1582c5046f94d5d5cea843c7e95a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Oct 2015 10:25:38 +0100 Subject: [PATCH 303/443] Put delete snippet btn after edit btn Signed-off-by: Dmitriy Zaporozhets --- app/views/snippets/_actions.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 751fafa894..1979ae6d5b 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,11 +1,11 @@ = link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do = icon('plus') New Snippet -- if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do - = icon('trash-o') - Delete - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do = icon('pencil-square-o') Edit +- if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do + = icon('trash-o') + Delete From e0e311a19c025435d119d379a98ec28f0704628f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Oct 2015 10:28:41 +0100 Subject: [PATCH 304/443] Fix bg for labels page when no labels present Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/labels/index.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 97175f8232..fb784ee5f4 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -14,8 +14,8 @@ = render @labels = paginate @labels, theme: 'gitlab' - else - .light-well + .nothing-here-block - if can? current_user, :admin_label, @project - .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels - else - .nothing-here-block No labels created + No labels created From ac006c193c0d5fbc1396b8f748688466fee4a184 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Thu, 29 Oct 2015 10:26:48 +0000 Subject: [PATCH 305/443] IE message doesn't need a heading --- doc/install/requirements.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index aa0d03b75b..5041ead601 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -109,8 +109,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- IE 10+ - -### Common UI problems with IE - -If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file +- IE 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file From ae99720a40b8e0700891f5828c1a93bcc7673e04 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Thu, 29 Oct 2015 10:27:21 +0000 Subject: [PATCH 306/443] Write out internet explorer --- doc/install/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 5041ead601..c0ccdd3745 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -109,4 +109,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- IE 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file +- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file From 7fc95d805dd2a4b997bbc3ad24a1a3a7d64ef305 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 20 Oct 2015 16:56:29 +0200 Subject: [PATCH 307/443] Added index on services.template This column is queried when creating a new project, without an index this query would lead to a sequence scan. --- db/migrate/20151020145526_add_services_template_index.rb | 5 +++++ db/schema.rb | 1 + 2 files changed, 6 insertions(+) create mode 100644 db/migrate/20151020145526_add_services_template_index.rb diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb new file mode 100644 index 0000000000..1b04f31356 --- /dev/null +++ b/db/migrate/20151020145526_add_services_template_index.rb @@ -0,0 +1,5 @@ +class AddServicesTemplateIndex < ActiveRecord::Migration + def change + add_index :services, :template + end +end diff --git a/db/schema.rb b/db/schema.rb index 4bde9f0b74..73fc83c3d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -667,6 +667,7 @@ ActiveRecord::Schema.define(version: 20151026182941) do add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree + add_index "services", ["template"], name: "index_services_on_template", using: :btree create_table "snippets", force: true do |t| t.string "title" From 6369c992a6d9279f4aa38c60c350c966f3926df1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 20 Oct 2015 17:44:15 +0200 Subject: [PATCH 308/443] Added benchmark for Projects::CreateService This benchmark currently runs at ~0.6 iterations per second and is unlikely to perform any better any time soon. --- .../services/projects/create_service_spec.rb | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 spec/benchmarks/services/projects/create_service_spec.rb diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb new file mode 100644 index 0000000000..25ed48c34f --- /dev/null +++ b/spec/benchmarks/services/projects/create_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::CreateService, benchmark: true do + describe '#execute' do + let(:user) { create(:user, :admin) } + + let(:group) do + group = create(:group) + + create(:group_member, group: group, user: user) + + group + end + + benchmark_subject do + name = SecureRandom.hex + service = described_class.new(user, + name: name, + path: name, + namespace_id: group.id, + visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + service.execute + end + + it { is_expected.to iterate_per_second(0.5) } + end +end From 29b3ce56ac45afc2c4dcd7055b53d8c06ef9faf2 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 21 Oct 2015 16:19:05 +0200 Subject: [PATCH 309/443] Removed extra activity update for new projects When a project is created the last activity timestamp is already set so there's no need for another update. --- app/services/projects/create_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index faf1ee008e..5b84527ecc 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -94,8 +94,6 @@ module Projects @project.team << [current_user, :master, current_user] end - @project.update_column(:last_activity_at, @project.created_at) - if @project.import? @project.import_start end From c5132e94e1c44884f8f9b399c4def20154b6c0ce Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 29 Oct 2015 14:21:24 +0100 Subject: [PATCH 310/443] Switch to gitlab-workhorse --- GITLAB_GIT_HTTP_SERVER_VERSION | 1 - GITLAB_WORKHORSE_VERSION | 1 + doc/update/8.1-to-8.2.md | 187 ++++++++++++++++++++++ lib/gitlab/backend/grack_auth.rb | 2 +- lib/support/init.d/gitlab | 68 ++++---- lib/support/init.d/gitlab.default.example | 11 +- lib/support/nginx/gitlab | 22 +-- lib/support/nginx/gitlab-ssl | 22 +-- 8 files changed, 249 insertions(+), 65 deletions(-) delete mode 100644 GITLAB_GIT_HTTP_SERVER_VERSION create mode 100644 GITLAB_WORKHORSE_VERSION create mode 100644 doc/update/8.1-to-8.2.md diff --git a/GITLAB_GIT_HTTP_SERVER_VERSION b/GITLAB_GIT_HTTP_SERVER_VERSION deleted file mode 100644 index 0d91a54c7d..0000000000 --- a/GITLAB_GIT_HTTP_SERVER_VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION new file mode 100644 index 0000000000..9e11b32fca --- /dev/null +++ b/GITLAB_WORKHORSE_VERSION @@ -0,0 +1 @@ +0.3.1 diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md new file mode 100644 index 0000000000..38e5661b36 --- /dev/null +++ b/doc/update/8.1-to-8.2.md @@ -0,0 +1,187 @@ +# From 8.1 to 8.2 + +**NOTE:** GitLab 8.0 introduced several significant changes related to +installation and configuration which *are not duplicated here*. Be sure you're +already running a working version of 8.0 before proceeding with this guide. + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version: + +```sh +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +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 8-2-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-2-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.5 +``` + +### 5. Replace gitlab-git-http-server with gitlab-workhorse + +Install and compile gitlab-workhorse. This requires [Go +1.5](https://golang.org/dl) which should already be on your system +from GitLab 8.1. + +```bash +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git +sudo -u git -H git checkout 0.3.1 +sudo -u git -H make +``` + +Update the GitLab init script and 'default' file. + +``` +cd /home/git/gitlab +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +test -e /etc/default/gitlab && \ + sudo sed -i .pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab +``` + +Make sure that you also update your **NGINX configuration** to use +the new gitlab-workhorse.socket file. + +### 6. 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 postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --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 +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-1-stable:config/gitlab.yml.example origin/8-2-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +View changes between the previous recommended Nginx configuration and the +current one: + +```sh +# For HTTPS configurations +git diff origin/8-1-stable:lib/support/nginx/gitlab-ssl origin/8-2-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-1-stable:lib/support/nginx/gitlab origin/8-2-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-git-http-server listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-2-stable/lib/support/init.d/gitlab.default.example#L34 + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. 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: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.0) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 7.14 to 8.0](7.14-to-8.0.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. + +## Troubleshooting + +### "You appear to have cloned an empty repository." + +See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting). diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 85a2d1a93a..440ef5a3cb 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -34,7 +34,7 @@ module Grack auth! if project && authorized_request? - # Tell gitlab-git-http-server the request is OK, and what the GL_ID is + # Tell gitlab-workhorse the request is OK, and what the GL_ID is render_grack_auth_ok elsif @user.nil? && !@ci unauthorized diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index a80e7e7743..f0a6c2b30e 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -37,10 +37,9 @@ web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" mail_room_enabled=false mail_room_pid_path="$pid_path/mail_room.pid" -gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid" -gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080" -gitlab_git_http_server_repo_root='/home/git/repositories' -gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log" +gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" +gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080" +gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" shell_path="/bin/bash" # Read configuration variable file if it is present @@ -76,8 +75,8 @@ check_pids(){ else spid=0 fi - if [ -f "$gitlab_git_http_server_pid_path" ]; then - hpid=$(cat "$gitlab_git_http_server_pid_path") + if [ -f "$gitlab_workhorse_pid_path" ]; then + hpid=$(cat "$gitlab_workhorse_pid_path") else hpid=0 fi @@ -94,7 +93,7 @@ check_pids(){ wait_for_pids(){ # We are sleeping a bit here mostly because sidekiq is slow at writing it's pid i=0; - while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_git_http_server_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do + while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do sleep 0.1; i=$((i+1)) if [ $((i%10)) = 0 ]; then @@ -131,9 +130,9 @@ check_status(){ fi if [ $hpid -ne 0 ]; then kill -0 "$hpid" 2>/dev/null - gitlab_git_http_server_status="$?" + gitlab_workhorse_status="$?" else - gitlab_git_http_server_status="-1" + gitlab_workhorse_status="-1" fi if [ "$mail_room_enabled" = true ]; then if [ $mpid -ne 0 ]; then @@ -143,7 +142,7 @@ check_status(){ mail_room_status="-1" fi fi - if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_git_http_server_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then + if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then gitlab_status=0 else # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html @@ -171,9 +170,9 @@ check_stale_pids(){ exit 1 fi fi - if [ "$hpid" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ]; then - echo "Removing stale gitlab-git-http-server pid. This is most likely caused by gitlab-git-http-server crashing the last time it ran." - if ! rm "$gitlab_git_http_server_pid_path"; then + if [ "$hpid" != "0" ] && [ "$gitlab_workhorse_status" != "0" ]; then + echo "Removing stale gitlab-workhorse pid. This is most likely caused by gitlab-workhorse crashing the last time it ran." + if ! rm "$gitlab_workhorse_pid_path"; then echo "Unable to remove stale pid, exiting" exit 1 fi @@ -190,7 +189,7 @@ check_stale_pids(){ ## If no parts of the service is running, bail out. exit_if_not_running(){ check_stale_pids - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then echo "GitLab is not running." exit fi @@ -206,8 +205,8 @@ start_gitlab() { if [ "$sidekiq_status" != "0" ]; then echo "Starting GitLab Sidekiq" fi - if [ "$gitlab_git_http_server_status" != "0" ]; then - echo "Starting gitlab-git-http-server" + if [ "$gitlab_workhorse_status" != "0" ]; then + echo "Starting gitlab-workhorse" fi if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then echo "Starting GitLab MailRoom" @@ -230,15 +229,14 @@ start_gitlab() { RAILS_ENV=$RAILS_ENV bin/background_jobs start & fi - if [ "$gitlab_git_http_server_status" = "0" ]; then - echo "The gitlab-git-http-server is already running with pid $spid, not restarting" + if [ "$gitlab_workhorse_status" = "0" ]; then + echo "The gitlab-workhorse is already running with pid $spid, not restarting" else - # No need to remove a socket, gitlab-git-http-server does this itself - $app_root/bin/daemon_with_pidfile $gitlab_git_http_server_pid_path \ - $app_root/../gitlab-git-http-server/gitlab-git-http-server \ - $gitlab_git_http_server_options \ - $gitlab_git_http_server_repo_root \ - >> $gitlab_git_http_server_log 2>&1 & + # No need to remove a socket, gitlab-workhorse does this itself + $app_root/bin/daemon_with_pidfile $gitlab_workhorse_pid_path \ + $app_root/../gitlab-workhorse/gitlab-workhorse \ + $gitlab_workhorse_options \ + >> $gitlab_workhorse_log 2>&1 & fi if [ "$mail_room_enabled" = true ]; then @@ -268,9 +266,9 @@ stop_gitlab() { echo "Shutting down GitLab Sidekiq" RAILS_ENV=$RAILS_ENV bin/background_jobs stop fi - if [ "$gitlab_git_http_server_status" = "0" ]; then - echo "Shutting down gitlab-git-http-server" - kill -- $(cat $gitlab_git_http_server_pid_path) + if [ "$gitlab_workhorse_status" = "0" ]; then + echo "Shutting down gitlab-workhorse" + kill -- $(cat $gitlab_workhorse_pid_path) fi if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then echo "Shutting down GitLab MailRoom" @@ -278,11 +276,11 @@ stop_gitlab() { fi # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. - while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do + while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do sleep 1 check_status printf "." - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then printf "\n" break fi @@ -292,7 +290,7 @@ stop_gitlab() { # Cleaning up unused pids rm "$web_server_pid_path" 2>/dev/null # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid. - rm -f "$gitlab_git_http_server_pid_path" + rm -f "$gitlab_workhorse_pid_path" if [ "$mail_room_enabled" = true ]; then rm "$mail_room_pid_path" 2>/dev/null fi @@ -303,7 +301,7 @@ stop_gitlab() { ## Prints the status of GitLab and it's components. print_status() { check_status - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then echo "GitLab is not running." return fi @@ -317,10 +315,10 @@ print_status() { else printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n" fi - if [ "$gitlab_git_http_server_status" = "0" ]; then - echo "The gitlab-git-http-server with pid $hpid is running." + if [ "$gitlab_workhorse_status" = "0" ]; then + echo "The gitlab-workhorse with pid $hpid is running." else - printf "The gitlab-git-http-server is \033[31mnot running\033[0m.\n" + printf "The gitlab-workhorse is \033[31mnot running\033[0m.\n" fi if [ "$mail_room_enabled" = true ]; then if [ "$mail_room_status" = "0" ]; then @@ -360,7 +358,7 @@ reload_gitlab(){ ## Restarts Sidekiq and Unicorn. restart_gitlab(){ check_status - if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then + if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then stop_gitlab fi start_gitlab diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index aab5acaa72..79ae8e0ae5 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -30,15 +30,14 @@ web_server_pid_path="$pid_path/unicorn.pid" # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" -gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid" -# The -listenXxx settings determine where gitlab-git-http-server +gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" +# The -listenXxx settings determine where gitlab-workhorse # listens for connections from NGINX. To listen on localhost:8181, write # '-listenNetwork tcp -listenAddr localhost:8181'. -# The -authBackend setting tells gitlab-git-http-server where it can reach +# The -authBackend setting tells gitlab-workhorse where it can reach # Unicorn. -gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080" -gitlab_git_http_server_repo_root="/home/git/repositories" -gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log" +gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080" +gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. # This is required for the Reply by email feature. diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 1e55c5a048..e767027dc2 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -38,8 +38,8 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } -upstream gitlab-git-http-server { - server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0; +upstream gitlab-workhorse { + server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } ## Normal HTTP host @@ -114,24 +114,24 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } - location @gitlab-git-http-server { + location @gitlab-workhorse { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. # gzip off; @@ -147,7 +147,7 @@ server { # The following settings only work with NGINX 1.7.11 or newer # - # # Pass chunked request bodies to gitlab-git-http-server as-is + # # Pass chunked request bodies to gitlab-workhorse as-is # proxy_request_buffering off; # proxy_http_version 1.1; @@ -156,7 +156,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://gitlab-git-http-server; + proxy_pass http://gitlab-workhorse; } ## Enable gzip compression as per rails guide: diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 08641bbcc1..4d31e31f8d 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -42,8 +42,8 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } -upstream gitlab-git-http-server { - server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0; +upstream gitlab-workhorse { + server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } ## Redirects all HTTP traffic to the HTTPS host @@ -161,24 +161,24 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { - # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block - error_page 418 = @gitlab-git-http-server; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; return 418; } - location @gitlab-git-http-server { + location @gitlab-workhorse { ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. gzip off; @@ -194,7 +194,7 @@ server { # The following settings only work with NGINX 1.7.11 or newer # - # # Pass chunked request bodies to gitlab-git-http-server as-is + # # Pass chunked request bodies to gitlab-workhorse as-is # proxy_request_buffering off; # proxy_http_version 1.1; @@ -203,7 +203,7 @@ server { proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://gitlab-git-http-server; + proxy_pass http://gitlab-workhorse; } ## Enable gzip compression as per rails guide: From b1547021bcceb1f622c42b0967d0aaf80cb14cd1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Oct 2015 14:22:11 +0100 Subject: [PATCH 311/443] Fix label destroy js Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/labels/destroy.js.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml index 1b4c83ab09..d59563b122 100644 --- a/app/views/projects/labels/destroy.js.haml +++ b/app/views/projects/labels/destroy.js.haml @@ -1,2 +1,2 @@ - if @project.labels.size == 0 - $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000) + $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) From 3be9d2c422b8651498abec3a2ee9bb6a3685f040 Mon Sep 17 00:00:00 2001 From: Ben Ford Date: Mon, 19 Oct 2015 14:52:46 -0700 Subject: [PATCH 312/443] Add ability to create directories in the editor Simply type a name with a `/` directory separator and new directories will be created. This does not do the fancy UI work that github.com does, but it will get the job done. I could not find tests for file creation, so I didn't add a test for this slight behaviour modification. I did test directory traversals though, using both absolute paths like `/tmp/foo.txt` and relative paths like `../../foo.txt`. Neither case escaped the repository, though attempting to traverse with a relative path resulted in a 500 error that did not affect application stability upon reload. --- CHANGELOG | 3 +++ app/assets/stylesheets/pages/editor.scss | 2 +- app/controllers/projects/blob_controller.rb | 2 +- app/services/files/create_dir_service.rb | 11 +++++++++++ app/services/files/create_service.rb | 13 +++++++++---- features/project/source/browse_files.feature | 10 ++++++++++ features/steps/project/source/browse_files.rb | 15 +++++++++++++++ lib/gitlab/regex.rb | 17 +++++++++++++++++ 8 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0d89fca9fc..200e105db4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +development + - Adds ability to create directories using the web editor + v 8.2.0 (unreleased) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 1d565477dd..e2c521af91 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -50,7 +50,7 @@ .editor-file-name { .new-file-name { display: inline-block; - width: 200px; + width: 450px; } .form-control { diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8cc2f21d88..93738aa1ee 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -161,7 +161,7 @@ class Projects::BlobController < Projects::ApplicationController if params[:file].present? params[:file_name] = params[:file].original_filename end - File.join(@path, File.basename(params[:file_name])) + File.join(@path, params[:file_name]) else @path end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 71272fb570..6107254a34 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -5,5 +5,16 @@ module Files def commit repository.commit_dir(current_user, @file_path, @commit_message, @target_branch) end + + def validate + super + + unless @file_path =~ Gitlab::Regex.file_path_regex + raise_error( + 'Your changes could not be committed, because the file path ' + + Gitlab::Regex.file_path_regex_message + ) + end + end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index c8e3a910bb..2348920cc5 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -9,12 +9,17 @@ module Files def validate super - file_name = File.basename(@file_path) - - unless file_name =~ Gitlab::Regex.file_name_regex + if @file_path =~ Gitlab::Regex.directory_traversal_regex raise_error( 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.file_name_regex_message + Gitlab::Regex.directory_traversal_regex_message + ) + end + + unless @file_path =~ Gitlab::Regex.file_path_regex + raise_error( + 'Your changes could not be committed, because the file name ' + + Gitlab::Regex.file_path_regex_message ) end diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 6b0484b6a3..69aa79f2d2 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -90,6 +90,16 @@ Feature: Project Source Browse Files Then I am on the new file page And I see a commit error message + @javascript + Scenario: I can create file with a directory name + Given I click on "New file" link in repo + And I fill the new file name with a new directory + And I edit code + And I fill the commit message + And I click on "Commit changes" + Then I am redirected to the new file with directory + And I should see its new content + @javascript Scenario: I can edit file 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 1b27500497..84725b9b58 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -78,6 +78,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps fill_in :file_name, with: 'Spaces Not Allowed' end + step 'I fill the new file name with a new directory' do + fill_in :file_name, with: new_file_name_with_directory + end + step 'I fill the commit message' do fill_in :commit_message, with: 'Not yet a commit message.', visible: true end @@ -238,6 +242,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps @project.namespace, @project, 'master/' + new_file_name)) end + step 'I am redirected to the new file with directory' do + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, 'master/' + new_file_name_with_directory)) + end + step 'I am redirected to the new file on new branch' do expect(current_path).to eq(namespace_project_blob_path( @project.namespace, @project, 'new_branch_name/' + new_file_name)) @@ -335,6 +344,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps 'not_a_file.md' end + # Constant value that is a valid filename with directory and + # not a filename present at root of the seed repository. + def new_file_name_with_directory + 'foo/bar/baz.txt' + end + # Constant value that is a valid directory and # not a directory present at root of the seed repository. def new_dir_name diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 9f1adc860d..53ab2686b4 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -51,6 +51,23 @@ module Gitlab "can contain only letters, digits, '_', '-' and '.'. " end + def file_path_regex + @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/]*\z/.freeze + end + + def file_path_regex_message + "can contain only letters, digits, '_', '-' and '.'. Separate directories with a '/'. " + end + + + def directory_traversal_regex + @directory_traversal_regex ||= /\.{2}/.freeze + end + + def directory_traversal_regex_message + "cannot include directory traversal. " + end + def archive_formats_regex # |zip|tar| tar.gz | tar.bz2 | From 6d0337a97a9d21a27bdd96b48ae17d2458b4ad46 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 29 Oct 2015 15:48:45 +0100 Subject: [PATCH 313/443] Update changelog item --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 200e105db4..325583722f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,5 @@ Please view this file on the master branch, on stable branches it's out of date. -development - - Adds ability to create directories using the web editor - v 8.2.0 (unreleased) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page @@ -32,6 +29,9 @@ v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) + - Adds ability to create directories using the web editor (Ben Ford) + +v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. From 6119b872fbb3b9dc19be3b21737dbb8380cf4847 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 29 Oct 2015 16:34:33 +0100 Subject: [PATCH 314/443] Add missing "cd" --- doc/update/8.1-to-8.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index 38e5661b36..d84d13f33e 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -79,6 +79,7 @@ from GitLab 8.1. ```bash cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git +cd gitlab-workhorse sudo -u git -H git checkout 0.3.1 sudo -u git -H make ``` From bc89c4f8d993028cd903217b6e1b989ca54307b9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 29 Oct 2015 16:34:50 +0100 Subject: [PATCH 315/443] Make sed command GNU-compatible --- doc/update/8.1-to-8.2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index d84d13f33e..3772f624e9 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -90,7 +90,7 @@ Update the GitLab init script and 'default' file. cd /home/git/gitlab sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab test -e /etc/default/gitlab && \ - sudo sed -i .pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab + sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab ``` Make sure that you also update your **NGINX configuration** to use From 49c081b9f38e99bbc11e7132d87773749b5b39d5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 28 Oct 2015 14:43:27 +0100 Subject: [PATCH 316/443] Improve performance of User.find_by_any_email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This query used to rely on a JOIN, effectively producing the following SQL: SELECT users.* FROM users LEFT OUTER JOIN emails ON emails.user_id = users.id WHERE (users.email = X OR emails.email = X) LIMIT 1; The use of a JOIN means having to scan over all Emails and users, join them together and then filter out the rows that don't match the criteria (though this step may be taken into account already when joining). In the new setup this query instead uses a sub-query, producing the following SQL: SELECT * FROM users WHERE id IN (select user_id FROM emails WHERE email = X) OR email = X LIMIT 1; This query has the benefit that it: 1. Doesn't have to JOIN any rows 2. Only has to operate on a relatively small set of rows from the "emails" table. Since most users will only have a handful of Emails associated (certainly not hundreds or even thousands) the size of the set returned by the sub-query is small enough that it should not become problematic. Performance of the old versus new version can be measured using the following benchmark: # Save this in ./bench.rb require 'benchmark/ips' email = 'yorick@gitlab.com' def User.find_by_any_email_old(email) user_table = arel_table email_table = Email.arel_table query = user_table. project(user_table[Arel.star]). join(email_table, Arel::Nodes::OuterJoin). on(user_table[:id].eq(email_table[:user_id])). where(user_table[:email].eq(email).or(email_table[:email].eq(email))) find_by_sql(query.to_sql).first end Benchmark.ips do |bench| bench.report 'original' do User.find_by_any_email_old(email) end bench.report 'optimized' do User.find_by_any_email(email) end bench.compare! end Running this locally using "bundle exec rails r bench.rb" produces the following output: Calculating ------------------------------------- original 1.000 i/100ms optimized 93.000 i/100ms ------------------------------------------------- original 11.103 (± 0.0%) i/s - 56.000 optimized 948.713 (± 5.3%) i/s - 4.743k Comparison: optimized: 948.7 i/s original: 11.1 i/s - 85.45x slower In other words, the new setup is 85x faster compared to the old setup, at least when running this benchmark locally. For GitLab.com these improvements result in User.find_by_any_email taking only ~170 ms to run, instead of around 800 ms. While this is "only" an improvement of about 4.5 times (instead of 85x) it's still significantly better than before. Fixes #3242 --- app/models/user.rb | 18 +++--------------- spec/benchmarks/models/user_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index c72beacbf0..924cb543fa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -235,21 +235,9 @@ class User < ActiveRecord::Base # Find a User by their primary email or any associated secondary email def find_by_any_email(email) - user_table = arel_table - email_table = Email.arel_table - - # Use ARel to build a query: - query = user_table. - # SELECT "users".* FROM "users" - project(user_table[Arel.star]). - # LEFT OUTER JOIN "emails" - join(email_table, Arel::Nodes::OuterJoin). - # ON "users"."id" = "emails"."user_id" - on(user_table[:id].eq(email_table[:user_id])). - # WHERE ("user"."email" = '' OR "emails"."email" = '') - where(user_table[:email].eq(email).or(email_table[:email].eq(email))) - - find_by_sql(query.to_sql).first + User.reorder(nil). + where('id IN (SELECT user_id FROM emails WHERE email = :email) OR email = :email', email: email). + take end def filter(filter_name) diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb index cc5c390419..74520aaf3f 100644 --- a/spec/benchmarks/models/user_spec.rb +++ b/spec/benchmarks/models/user_spec.rb @@ -39,4 +39,30 @@ describe User, benchmark: true do it { is_expected.to iterate_per_second(iterations) } end end + + describe '.find_by_any_email' do + let(:user) { create(:user) } + + describe 'using a user with only a single Email address' do + let(:email) { user.email } + + benchmark_subject { User.find_by_any_email(email) } + + it { is_expected.to iterate_per_second(5000) } + end + + describe 'using a user with multiple Email addresses' do + let(:email) { user.emails.first.email } + + benchmark_subject { User.find_by_any_email(email) } + + before do + 10.times do + user.emails.create(email: FFaker::Internet.email) + end + end + + it { is_expected.to iterate_per_second(5000) } + end + end end From 0a6aaf256900ab47058d03d3b74883420c4643ae Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 28 Oct 2015 14:57:52 +0100 Subject: [PATCH 317/443] Changelog entry for User.find_by_any_email --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index cf44a5c7c8..6bec4f606e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) + - Improved performance of finding users by one of their Email addresses - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL From 24c8f42278844cf48cdd9871b8285445379623f0 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 29 Oct 2015 12:05:47 +0100 Subject: [PATCH 318/443] Use a UNION for User.find_by_any_email This is significantly faster than using a sub-query, at least when run on the GitLab.com production database. The benchmarks are a lot slower now with these changes, most likely due to PostgreSQL choosing a different (and less efficient) plan based on the amount of data present in the test database. Thanks to @dlemstra for suggesting the use of a UNION. --- app/models/user.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 924cb543fa..35f5ab5679 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -235,9 +235,18 @@ class User < ActiveRecord::Base # Find a User by their primary email or any associated secondary email def find_by_any_email(email) - User.reorder(nil). - where('id IN (SELECT user_id FROM emails WHERE email = :email) OR email = :email', email: email). - take + # Arel doesn't allow for chaining operations on union nodes, thus we have + # to write this query by hand. See the following issue for more info: + # https://github.com/rails/arel/issues/98. + sql = '(SELECT * FROM users WHERE email = :email + UNION + SELECT users.* + FROM emails + INNER JOIN users ON users.id = emails.user_id + WHERE emails.email = :email) + LIMIT 1;' + + User.find_by_sql([sql, { email: email }]).first end def filter(filter_name) From bba46623c20de52942c85eb2aba3649e3f4ba296 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 29 Oct 2015 13:33:20 +0100 Subject: [PATCH 319/443] Fixed UNION syntax for MySQL MySQL doesn't support the previous syntax. --- app/models/user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 35f5ab5679..66db70080b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -238,9 +238,9 @@ class User < ActiveRecord::Base # Arel doesn't allow for chaining operations on union nodes, thus we have # to write this query by hand. See the following issue for more info: # https://github.com/rails/arel/issues/98. - sql = '(SELECT * FROM users WHERE email = :email + sql = '(SELECT * FROM users WHERE email = :email) UNION - SELECT users.* + (SELECT users.* FROM emails INNER JOIN users ON users.id = emails.user_id WHERE emails.email = :email) From a9df714764d6138bf162acd82b780ca82a21864b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 29 Oct 2015 17:51:49 +0100 Subject: [PATCH 320/443] Use a subquery with IDs only for find_by_any_email This further improves performance of User.find_by_any_email and is roughly twice as fast as the previous UNION setup. Thanks again to @dlemstra for suggesting this. --- app/models/user.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 66db70080b..67fef1c1e6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -235,15 +235,13 @@ class User < ActiveRecord::Base # Find a User by their primary email or any associated secondary email def find_by_any_email(email) - # Arel doesn't allow for chaining operations on union nodes, thus we have - # to write this query by hand. See the following issue for more info: - # https://github.com/rails/arel/issues/98. - sql = '(SELECT * FROM users WHERE email = :email) - UNION - (SELECT users.* - FROM emails - INNER JOIN users ON users.id = emails.user_id - WHERE emails.email = :email) + sql = 'SELECT * + FROM users + WHERE id IN ( + SELECT id FROM users WHERE email = :email + UNION + SELECT emails.user_id FROM emails WHERE email = :email + ) LIMIT 1;' User.find_by_sql([sql, { email: email }]).first From 6d3068bec3a926d17f4f2d0da895856489bfcb7a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 29 Oct 2015 17:53:56 +0100 Subject: [PATCH 321/443] Adjusted ips/sec for find_by_any_email benchmarks While these benchmarks run at roughly 1500 i/sec setting the threshold to 1000 leaves some room for deviations (e.g. due to different DB setups). --- spec/benchmarks/models/user_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb index 74520aaf3f..4cdba66939 100644 --- a/spec/benchmarks/models/user_spec.rb +++ b/spec/benchmarks/models/user_spec.rb @@ -48,7 +48,7 @@ describe User, benchmark: true do benchmark_subject { User.find_by_any_email(email) } - it { is_expected.to iterate_per_second(5000) } + it { is_expected.to iterate_per_second(1000) } end describe 'using a user with multiple Email addresses' do @@ -62,7 +62,7 @@ describe User, benchmark: true do end end - it { is_expected.to iterate_per_second(5000) } + it { is_expected.to iterate_per_second(1000) } end end end From 72ececfab18d4577fbd7e57d44b771576eb204bf Mon Sep 17 00:00:00 2001 From: Thirumal S Date: Fri, 30 Oct 2015 17:27:22 +0530 Subject: [PATCH 322/443] form-control class to select box --- app/views/profiles/notifications/_settings.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml index 2c85d2a9b2..742c5c4b68 100644 --- a/app/views/profiles/notifications/_settings.html.haml +++ b/app/views/profiles/notifications/_settings.html.haml @@ -14,4 +14,4 @@ = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'trigger-submit' + = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' From 74416daa660a047d3a7cb00e11e1d775b4ea0937 Mon Sep 17 00:00:00 2001 From: Thirumal S Date: Fri, 30 Oct 2015 19:26:56 +0530 Subject: [PATCH 323/443] form group alignment issue fixed in webhook index page --- app/assets/stylesheets/pages/projects.scss | 4 ++++ app/views/projects/hooks/index.html.haml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f7a2284900..b384e3fae6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -511,3 +511,7 @@ pre.light-well { margin-top: -1px; } } + +.form-control-padding-top { + padding-top: 10px; +} diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 85dbfd6786..65e00b38ad 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -19,7 +19,7 @@ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' .form-group = f.label :url, "Trigger", class: 'control-label' - .col-sm-10 + .col-sm-10.form-control-padding-top %div = f.check_box :push_events, class: 'pull-left' .prepend-left-20 From 31ea88cfed2580975f621a6aa992ac58405cb275 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Sat, 31 Oct 2015 02:27:44 +0900 Subject: [PATCH 324/443] Fix deadlink in docs for ci/examples --- doc/ci/examples/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index e0b9fa0e25..c8122fc63b 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -1,5 +1,5 @@ # Build script examples -+ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md) -+ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md) -+ [Test Clojure applications](examples/test-clojure-application.md) ++ [Test and deploy Ruby applications to Heroku](test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy Python applications to Heroku](test-and-deploy-python-application-to-heroku.md) ++ [Test Clojure applications](test-clojure-application.md) From 31723eb9f0f9490d873a6ecddc897fef3ea1885c Mon Sep 17 00:00:00 2001 From: KON YUICHI Date: Sat, 31 Oct 2015 22:32:06 +0900 Subject: [PATCH 325/443] fix deprecated --- app/controllers/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 05c7d3de8b..00d13a83ce 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,7 @@ class ProjectsController < ApplicationController include ExtractsPath - prepend_before_filter :render_go_import, only: [:show] + prepend_before_action :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] From 3b0039f659eab1c29e735ef2a5443e26b33d5d18 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 31 Oct 2015 16:06:06 +0100 Subject: [PATCH 326/443] Persist blob editor's value on submit, not on click Prior, the value of the Ace editor was only being persisted if the user physically clicked the submit button, which the "quick submit" behavior doesn't do. Now the value will be properly transferred before any form is submitted. --- app/assets/javascripts/blob/edit_blob.js.coffee | 8 ++++---- app/assets/javascripts/blob/new_blob.js.coffee | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 050888f9c1..f6bf836f19 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -11,10 +11,10 @@ class @EditBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - $(".js-commit-button").click -> - $("#file-content").val editor.getValue() - $(".file-editor form").submit() - return false + # Before a form submission, move the content from the Ace editor into the + # submitted textarea + $('form').submit -> + $("#file-content").val(editor.getValue()) editModePanes = $(".js-edit-mode-pane") editModeLinks = $(".js-edit-mode a") diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index 1f36a53f19..68c5e5195e 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -11,10 +11,10 @@ class @NewBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - $(".js-commit-button").click -> - $("#file-content").val editor.getValue() - $(".file-editor form").submit() - return false + # Before a form submission, move the content from the Ace editor into the + # submitted textarea + $('form').submit -> + $("#file-content").val(editor.getValue()) editor: -> return @editor From 98008a2db85a5320d885cd515557d4093a1cba24 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sat, 31 Oct 2015 22:56:24 +0100 Subject: [PATCH 327/443] Fix typo in rake task doc --- doc/development/rake_tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index a4a980cf0e..9f3fd69fc4 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -9,7 +9,7 @@ bundle exec rake setup ``` The `setup` task is a alias for `gitlab:setup`. -This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and fianlly it calls `db:seed_fu` to seed the database. +This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. Note: `db:setup` calls `db:seed` but this does nothing. ## Run tests From a1d0eca88686faabd9742f92174a0caa76e36cf6 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sat, 31 Oct 2015 23:31:21 +0100 Subject: [PATCH 328/443] Gilab -> GitLab --- doc/ci/api/README.md | 2 +- doc/ci/api/projects.md | 22 +++++++++---------- doc/ci/api/runners.md | 4 ++-- doc/install/installation.md | 2 +- doc/operations/unicorn.md | 2 +- doc/ssh/README.md | 7 +++--- .../import_projects_from_gitlab_com.md | 4 ++-- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index 33c5b172e9..cf9710ede5 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -25,7 +25,7 @@ GitLab CI API has 4 authentication methods: Authentication is done by sending the `private-token` of a valid user and the `url` of an -authorized Gitlab instance via a query string along with the API +authorized GitLab instance via a query string along with the API request: GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/ diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md index 5585191e82..74a4c64d00 100644 --- a/doc/ci/api/projects.md +++ b/doc/ci/api/projects.md @@ -1,7 +1,7 @@ # Projects API This API is intended to aid in the setup and configuration of -projects on Gitlab CI. +projects on GitLab CI. __Authentication is done by GitLab user token & GitLab url__ @@ -88,23 +88,23 @@ authorized. Parameters: - * `id` (required) - The ID of the Gitlab CI project + * `id` (required) - The ID of the GitLab CI project ### Create Project -Creates a Gitlab CI project using Gitlab project details. +Creates a GitLab CI project using GitLab project details. POST /ci/projects Parameters: * `name` (required) - The name of the project - * `gitlab_id` (required) - The ID of the project on the Gitlab instance + * `gitlab_id` (required) - The ID of the project on the GitLab instance * `default_ref` (optional) - The branch to run on (default to `master`) ### Update Project -Updates a Gitlab CI project using Gitlab project details that the +Updates a GitLab CI project using GitLab project details that the authenticated user has access to. PUT /ci/projects/:id @@ -116,13 +116,13 @@ Parameters: ### Remove Project -Removes a Gitlab CI project that the authenticated user has access to. +Removes a GitLab CI project that the authenticated user has access to. DELETE /ci/projects/:id Parameters: - * `id` (required) - The ID of the Gitlab CI project + * `id` (required) - The ID of the GitLab CI project ### Link Project to Runner @@ -133,8 +133,8 @@ authorized user). Parameters: - * `id` (required) - The ID of the Gitlab CI project - * `runner_id` (required) - The ID of the Gitlab CI runner + * `id` (required) - The ID of the GitLab CI project + * `runner_id` (required) - The ID of the GitLab CI runner ### Remove Project from Runner @@ -145,5 +145,5 @@ via authorized user). Parameters: - * `id` (required) - The ID of the Gitlab CI project - * `runner_id` (required) - The ID of the Gitlab CI runner \ No newline at end of file + * `id` (required) - The ID of the GitLab CI project + * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md index e9f88ee066..c383dc4bcc 100644 --- a/doc/ci/api/runners.md +++ b/doc/ci/api/runners.md @@ -6,7 +6,7 @@ __Authentication is done by GitLab user token & GitLab url__ -Used to get information about all runners registered on the Gitlab CI +Used to get information about all runners registered on the GitLab CI instance. GET /ci/runners @@ -31,7 +31,7 @@ Returns: __Authentication is done with a Shared runner registration token or a project Specific runner registration token__ -Used to make Gitlab CI aware of available runners. +Used to make GitLab CI aware of available runners. POST /ci/runners/register diff --git a/doc/install/installation.md b/doc/install/installation.md index b48acb655e..b02e8e8e58 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -489,7 +489,7 @@ See the [omniauth integration document](../integration/omniauth.md) ### Build your projects GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. -Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it +Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it ### Custom Redis Connection diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md index 31b432cd41..3998da01f0 100644 --- a/doc/operations/unicorn.md +++ b/doc/operations/unicorn.md @@ -78,7 +78,7 @@ threshold is a random value between 200 and 250 MB. The master process (PID ``` One other thing that stands out in the log snippet above, taken from -Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This +GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This is a normal value for our current GitLab.com setup and traffic. The high frequency of Unicorn memory restarts on some GitLab sites can be a diff --git a/doc/ssh/README.md b/doc/ssh/README.md index b6b8000af4..0bdb4070e7 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -15,8 +15,7 @@ Note: It is a best practice to use a password for an SSH key, but it is not required and you can skip creating a password by pressing enter. Note that the password you choose here can't be altered or retrieved. -To generate a new SSH key, use the following command: -```bash +To generate a new SSH key, use the following commandGitLab```bash ssh-keygen -t rsa -C "$your_email" ``` This command will prompt you for a location and filename to store the key @@ -82,7 +81,7 @@ How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Ecli ## Tip: Non-default OpenSSH key file names or locations -If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: +If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: ``` # @@ -97,7 +96,7 @@ User mygitlabusername Another example ``` # -# Our company's internal Gitlab server +# Our company's internal GitLab server # Host my-gitlab.company.com RSAAuthentication yes diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index f4c4e955d4..1117db98e7 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -2,12 +2,12 @@ You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if GitLab support is enabled on your GitLab instance. -You can read more about Gitlab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) +You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. ![New project page](gitlab_importer/new_project_page.png) -Click on the "Import projects from Gitlab.com" link and you will be redirected to GitLab.com +Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com for permission to access your projects. After accepting, you'll be automatically redirected to the importer. From 1045a72eb7fd16ab09b7e68e1a467ff0f5c9215d Mon Sep 17 00:00:00 2001 From: zheng_b Date: Mon, 2 Nov 2015 07:59:04 +0100 Subject: [PATCH 329/443] Go to gitlab installation folder before initialize database --- doc/install/installation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/install/installation.md b/doc/install/installation.md index b48acb655e..8ed1eba560 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -329,6 +329,10 @@ GitLab Shell is an SSH access and repository management software developed speci sudo -u git -H make ### Initialize Database and Activate Advanced Features + + # Go to Gitlab installation folder + + cd /home/git/gilab sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production From c03da1caade637298ca96e59cea990b0827539c9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 Nov 2015 14:44:06 +0100 Subject: [PATCH 330/443] Extend yml syntax for only and except to support specifying repository path --- CHANGELOG | 1 + app/models/ci/commit.rb | 2 +- doc/ci/yaml/README.md | 14 +- lib/ci/gitlab_ci_yaml_processor.rb | 64 ++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 327 +++++++++++++------ 5 files changed, 285 insertions(+), 123 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6bec4f606e..eec74e5c5f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.2.0 (unreleased) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page + - Extend yml syntax for only and except to support specifying repository path - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 13437b2483..e58420d82d 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -187,7 +187,7 @@ module Ci end def config_processor - @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file) + @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace) rescue Ci::GitlabCiYamlProcessor::ValidationError => e save_yaml_error(e.message) nil diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ea8f72bc13..0c1d8cfdff 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -169,7 +169,7 @@ This are two parameters that allow for setting a refs policy to limit when jobs There are a few rules that apply to usage of refs policy: -1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account. +1. `only` and `except` are inclusive. If both `only` and `except` are defined in job specification the ref is filtered by `only` and `except`. 1. `only` and `except` allow for using the regexp expressions. 1. `only` and `except` allow for using special keywords: `branches` and `tags`. These names can be used for example to exclude all tags and all branches. @@ -182,6 +182,18 @@ job: - branches # use special keyword ``` +1. `only` and `except` allow for specify repository path to filter jobs for forks. +The repository path can be used to have jobs executed only for parent repository. + +```yaml +job: + only: + - branches@gitlab-org/gitlab-ce + except: + - master@gitlab-org/gitlab-ce +``` +The above will run `job` for all branches, except master on `gitlab-org/gitlab-ce` repository only. + ### tags `tags` is used to select specific runners from the list of all runners that are allowed to run this project. diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index efcd2faffc..0f57a4f53a 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -7,10 +7,11 @@ module Ci ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables] ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when] - attr_reader :before_script, :image, :services, :variables + attr_reader :before_script, :image, :services, :variables, :path - def initialize(config) + def initialize(config, path = nil) @config = YAML.load(config) + @path = path unless @config.is_a? Hash raise ValidationError, "YAML should be a hash" @@ -63,26 +64,6 @@ module Ci end end - def process?(only_params, except_params, ref, tag) - return true if only_params.nil? && except_params.nil? - - if only_params - return true if tag && only_params.include?("tags") - return true if !tag && only_params.include?("branches") - - only_params.find do |pattern| - match_ref?(pattern, ref) - end - else - return false if tag && except_params.include?("tags") - return false if !tag && except_params.include?("branches") - - except_params.each do |pattern| - return false if match_ref?(pattern, ref) - end - end - end - def build_job(name, job) { stage_idx: stages.index(job[:stage]), @@ -101,14 +82,6 @@ module Ci } end - def match_ref?(pattern, ref) - if pattern.first == "/" && pattern.last == "/" - Regexp.new(pattern[1...-1]) =~ ref - else - pattern == ref - end - end - def normalize_script(script) if script.is_a? Array script.join("\n") @@ -208,5 +181,36 @@ module Ci def validate_string(value) value.is_a?(String) || value.is_a?(Symbol) end + + def process?(only_params, except_params, ref, tag) + if only_params.present? + return false unless matching?(only_params, ref, tag) + end + + if except_params.present? + return false if matching?(except_params, ref, tag) + end + + true + end + + def matching?(patterns, ref, tag) + patterns.any? do |pattern| + match_ref?(pattern, ref, tag) + end + end + + def match_ref?(pattern, ref, tag) + pattern, path = pattern.split('@', 2) + return false if path && path != self.path + return true if tag && pattern == 'tags' + return true if !tag && pattern == 'branches' + + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ ref + else + pattern == ref + end + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index abdb6b89ac..a141a459ba 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' module Ci describe GitlabCiYamlProcessor do - + let(:path) { 'path' } + describe "#builds_for_ref" do let(:type) { 'test' } @@ -12,7 +13,7 @@ module Ci rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ @@ -28,78 +29,222 @@ module Ci when: "on_success" }) end + + describe :only do + it "does not return builds if only has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["deploy"] } + }) - it "does not return builds if only has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["deploy"] } - }) + config_processor = GitlabCiYamlProcessor.new(config, path) - config_processor = GitlabCiYamlProcessor.new(config) + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + it "does not return builds if only has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["/^deploy$/"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "returns builds if only has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["master"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if only has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["master", "deploy"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns builds if only has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["tags"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if only has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@path"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@fork"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns build only for specified type" do + + config = YAML.dump({ + before_script: ["pwd"], + build: { script: "build", type: "build", only: ["master", "deploy"] }, + rspec: { script: "rspec", type: type, only: ["master", "deploy"] }, + staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, + production: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, + production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }, + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + end end - it "does not return builds if only has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["/^deploy$/"] } - }) + describe :except do + it "returns builds if except has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["deploy"] } + }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if except has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["/^deploy$/"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "does not return builds if except has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["master"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if except has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["master", "deploy"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "does not return builds if except has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["tags"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if except has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@path"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@fork"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns build except specified type" do + config = YAML.dump({ + before_script: ["pwd"], + build: { script: "build", type: "build", except: ["master", "deploy"] }, + rspec: { script: "rspec", type: type, except: ["master", "deploy", "test@fork"] }, + staging: { script: "deploy", type: "deploy", except: ["master", "deploy"] }, + production: { script: "deploy", type: "deploy", except: ["master", "deploy"] }, + production: { script: "deploy", type: "deploy", except: ["master@path", "deploy"] }, + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref(type, "test").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(2) + end end - it "returns builds if only has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "does not build tags" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0) - end - - it "returns builds if only has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["master", "deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns build only for specified type" do - - config = YAML.dump({ - before_script: ["pwd"], - build: { script: "build", type: "build", only: ["master", "deploy"] }, - rspec: { script: "rspec", type: type, only: ["master", "deploy"] }, - staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, - production: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - end end describe "Image and service handling" do @@ -111,7 +256,7 @@ module Ci rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ @@ -139,7 +284,7 @@ module Ci rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ @@ -172,7 +317,7 @@ module Ci rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) expect(config_processor.variables).to eq(variables) end end @@ -184,7 +329,7 @@ module Ci rspec: { script: "rspec", when: when_state } }) - config_processor = GitlabCiYamlProcessor.new(config) + config_processor = GitlabCiYamlProcessor.new(config, path) builds = config_processor.builds_for_stage_and_ref("test", "master") expect(builds.size).to eq(1) expect(builds.first[:when]).to eq(when_state) @@ -200,154 +345,154 @@ module Ci it "returns errors if tags parameter is invalid" do config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings") end it "returns errors if before_script parameter is invalid" do config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings") end it "returns errors if image parameter is invalid" do config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string") end it "returns errors if job name is blank" do config = YAML.dump({ '' => { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") end it "returns errors if job name is non-string" do config = YAML.dump({ 10 => { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") end it "returns errors if job image parameter is invalid" do config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string") end it "returns errors if services parameter is not an array" do config = YAML.dump({ services: "test", rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") end it "returns errors if services parameter is not an array of strings" do config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") end it "returns errors if job services parameter is not an array" do config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") end it "returns errors if there are unknown parameters" do config = YAML.dump({ extra: "bundle update" }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") end it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do config = YAML.dump({ extra: { services: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") end it "returns errors if there is no any jobs defined" do config = YAML.dump({ before_script: ["bundle update"] }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job") end it "returns errors if job allow_failure parameter is not an boolean" do config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean") end it "returns errors if job stage is not a string" do config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a pre-defined stage" do config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") end it "returns errors if stages is not an array" do config = YAML.dump({ types: "test", rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") end it "returns errors if stages is not an array of strings" do config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") end it "returns errors if variables is not a map" do config = YAML.dump({ variables: "test", rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") end it "returns errors if variables is not a map of key-valued strings" do config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") end it "returns errors if job when is not on_success, on_failure or always" do config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do - GitlabCiYamlProcessor.new(config) + GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end end From 2dec5ec99042cd8da6c127d4bcfa7f5f84ef94eb Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 28 Oct 2015 17:39:22 +0100 Subject: [PATCH 331/443] Only redirect to homepage url when its not the root url It was possible to create an infi redirect when the user set up the `home_page_url` to redirect to the main URL of the gitlab instance. This fix makes sure this redirect is not possible. Fixes !1020 Signed-off-by: Jeroen van Baarsen --- app/controllers/application_controller.rb | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1b0609e279..0d182e8eb0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -59,13 +59,8 @@ class ApplicationController < ActionController::Base end def authenticate_user!(*args) - # If user is not signed-in and tries to access root_path - redirect him to landing page - # Don't redirect to the default URL to prevent endless redirections - if current_application_settings.home_page_url.present? && - current_application_settings.home_page_url.chomp('/') != Gitlab.config.gitlab['url'].chomp('/') - if current_user.nil? && root_path == request.path - redirect_to current_application_settings.home_page_url and return - end + if redirect_to_home_page_url? + redirect_to current_application_settings.home_page_url and return end super(*args) @@ -346,4 +341,17 @@ class ApplicationController < ActionController::Base def git_import_enabled? current_application_settings.import_sources.include?('git') end + + def redirect_to_home_page_url? + # If user is not signed-in and tries to access root_path - redirect him to landing page + # Don't redirect to the default URL to prevent endless redirections + return false unless current_application_settings.home_page_url.present? + + home_page_url = current_application_settings.home_page_url.chomp('/') + root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] + + return false if root_urls.include?(home_page_url) + + current_user.nil? && root_path == request.path + end end From 1056c9d2d2481ee4fe3a55e0d9acb3691c1a019c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 Nov 2015 15:49:54 +0100 Subject: [PATCH 332/443] Spelling --- doc/ci/yaml/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 0c1d8cfdff..d117a2969b 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -192,7 +192,7 @@ job: except: - master@gitlab-org/gitlab-ce ``` -The above will run `job` for all branches, except master on `gitlab-org/gitlab-ce` repository only. +The above will run `job` for all branches on `gitlab-org/gitlab-ce`, except master . ### tags `tags` is used to select specific runners from the list of all runners that are allowed to run this project. From e032a7288f4103a6d5980da6af4040bc9271bd99 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 2 Nov 2015 16:02:47 +0100 Subject: [PATCH 333/443] Spread out runner contacted_at updates --- lib/ci/api/helpers.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index e602cda81d..7e4986b6af 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -16,7 +16,9 @@ module Ci end def update_runner_last_contact - if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY + # Use a random threshold to prevent beating DB updates + contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) + if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age current_runner.update_attributes(contacted_at: Time.now) end end From 1b8d324762787805fabee72d585ac40e244af4d7 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 2 Nov 2015 14:14:38 +0200 Subject: [PATCH 334/443] Add ability to fetch the commit ID of the last commit that actually touched a file --- CHANGELOG | 1 + doc/api/repository_files.md | 3 ++- lib/api/files.rb | 4 +++- spec/requests/api/files_spec.rb | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6bec4f606e..588909d257 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.2.0 (unreleased) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. + - [API] Add ability to fetch the commit ID of the last commit that actually touched a file v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 25311b0710..623063f357 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -23,7 +23,8 @@ Example response: "content": "IyA9PSBTY2hlbWEgSW5mb3...", "ref": "master", "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", - "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50" + "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", + "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" } ``` diff --git a/lib/api/files.rb b/lib/api/files.rb index 308c84dd13..a7a768f889 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -43,7 +43,8 @@ module API # "content": "IyA9PSBTY2hlbWEgSW5mb3...", # "ref": "master", # "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", - # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50" + # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", + # "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", # } # get ":id/repository/files" do @@ -71,6 +72,7 @@ module API ref: ref, blob_id: blob.id, commit_id: commit.id, + last_commit_id: user_project.repository.last_commit_for_path(commit.sha, file_path).id } else not_found! 'File' diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 042e635256..8efa09f75f 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -19,6 +19,7 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response['file_path']).to eq(file_path) expect(json_response['file_name']).to eq('popen.rb') + expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end From 810c91fe35db6a83c9c517e03d07dc1795922646 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Nov 2015 16:39:24 +0100 Subject: [PATCH 335/443] Refactor search by commits message Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 4 +-- app/assets/stylesheets/pages/commits.scss | 2 ++ app/models/repository.rb | 6 ++-- app/views/search/_category.html.haml | 2 +- app/views/search/results/_commits.html.haml | 32 +-------------------- lib/gitlab/project_search_results.rb | 2 +- spec/models/repository_spec.rb | 4 +-- 7 files changed, 11 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b573d40317..78e0891e82 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.2.0 (unreleased) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. + - Include commit logs in project search v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) @@ -29,9 +30,6 @@ v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - - Include commit logs in project search - -v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 4e121b95d1..e485487bcf 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -33,6 +33,8 @@ } li.commit { + list-style: none; + .commit-row-title { font-size: $list-font-size; line-height: 20px; diff --git a/app/models/repository.rb b/app/models/repository.rb index a0f2b3fb76..c6d904339e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -87,12 +87,12 @@ class Repository commits end - def find_commits_with_matching_log(query) + def find_commits_by_message(query) # Limited to 1000 commits for now, could be parameterized? args = %W(git log --pretty=%H --max-count 1000 --grep=#{query}) - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map{ |l| l.chomp } - commits = git_log_results.map{ |c| commit(c) } + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) + commits = git_log_results.map { |c| commit(c) } commits end diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index ef43f727d8..481451edb2 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -46,7 +46,7 @@ = link_to search_filter_path(scope: 'commits') do = icon('history fw') %span - Commit Logs + Commits %span.badge = @search_results.commits_count diff --git a/app/views/search/results/_commits.html.haml b/app/views/search/results/_commits.html.haml index 8076174e59..7cff694350 100644 --- a/app/views/search/results/_commits.html.haml +++ b/app/views/search/results/_commits.html.haml @@ -1,32 +1,2 @@ .search-result-row - .commits-row-title - %strong.str-truncated - = link_to commits.title, namespace_project_commit_path(@project.namespace, @project, commits.id), class: "commit_short_id" - - .pull-right - = link_to commits.short_id, namespace_project_commit_path(@project.namespace, @project, commits.id), class: "commit_short_id" - - .notes_count - - if @note_counts - - note_count = @note_counts.fetch(commits.id, 0) - - else - - notes = commits.notes - - note_count = notes.user.count - - - if note_count > 0 - %span.light - %i.fa.fa-comments - = note_count - - - if commits.description? - .commits-row-description - %pre - = preserve(gfm(escape_once(commits.description))) - - .commits-row-info - = commit_author_link(commits, avatar: true, size: 24) - authored - .committed_ago - #{time_ago_with_tooltip(commits.committed_date)}   - = link_to_browse_code(@project, commits) - %br + = render 'projects/commits/commit', project: @project, commit: commits diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 54389f7d66..c873ef3771 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -77,7 +77,7 @@ module Gitlab end def commits - project.repository.find_commits_with_matching_log(query) + project.repository.find_commits_by_message(query) end def limit_project_ids diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index aedbfa04d8..319fa0a7c8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -26,8 +26,8 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end - describe :find_commits_with_matching_log do - subject { repository.find_commits_with_matching_log('submodule').map{ |k| k.id } } + describe :find_commits_by_message do + subject { repository.find_commits_by_message('submodule').map{ |k| k.id } } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } From 63cafdb8e5f9481539beaf2b6eac11cf8148daf1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 2 Nov 2015 17:04:28 +0100 Subject: [PATCH 336/443] Add changelog entry for contacted_at --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6bec4f606e..b67f010c6a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,9 @@ v 8.2.0 (unreleased) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. +v 8.1.3 + - Spread out runner contacted_at updates + v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory From 60c5b52ee906639e8bc0408a4a63d50862580fc8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 2 Nov 2015 15:04:43 -0500 Subject: [PATCH 337/443] Explicitly require backup/files --- lib/backup/builds.rb | 2 ++ lib/backup/uploads.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb index 800f30c214..635967f4bd 100644 --- a/lib/backup/builds.rb +++ b/lib/backup/builds.rb @@ -1,3 +1,5 @@ +require 'backup/files' + module Backup class Builds < Files def initialize diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 0a0ec564ba..9261f77f3c 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -1,3 +1,5 @@ +require 'backup/files' + module Backup class Uploads < Files From f61aa9acdc54306f22bbf4209bdae75b35d7df0c Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Tue, 3 Nov 2015 14:01:27 +0900 Subject: [PATCH 338/443] Update links in CI docs after GitLab 8.1 --- doc/ci/examples/README.md | 6 +++--- .../test-and-deploy-python-application-to-heroku.md | 2 +- .../examples/test-and-deploy-ruby-application-to-heroku.md | 4 ++-- doc/ci/examples/test-clojure-application.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index c8122fc63b..1cf41aea39 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -1,5 +1,5 @@ # Build script examples -+ [Test and deploy Ruby applications to Heroku](test-and-deploy-ruby-application-to-heroku.md) -+ [Test and deploy Python applications to Heroku](test-and-deploy-python-application-to-heroku.md) -+ [Test Clojure applications](test-clojure-application.md) ++ [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) ++ [Test a Clojure application](test-clojure-application.md) diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index 036b03dd6b..a236da53fe 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -1,7 +1,7 @@ ## Test and Deploy a python application This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application. -You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080). +You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all). ### Configure project This is what the `.gitlab-ci.yml` file looks like for this project: diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index d2a872f193..e52e154746 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -1,7 +1,7 @@ ## Test and Deploy a ruby application This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application. -You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050). +You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all). ### Configure project This is what the `.gitlab-ci.yml` file looks like for this project: @@ -64,4 +64,4 @@ gitlab-ci-multi-runner register \ With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. -To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. \ No newline at end of file +To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md index eaee94a10f..56b746ce02 100644 --- a/doc/ci/examples/test-clojure-application.md +++ b/doc/ci/examples/test-clojure-application.md @@ -1,8 +1,8 @@ -## Test Clojure applications +## Test a Clojure application This example will guide you how to run tests in your Clojure application. -You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306). +You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all). ### Configure project From d0398514b8b8eda7d03ba89b250554ff60a29ed4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Nov 2015 15:36:04 +0100 Subject: [PATCH 339/443] Add 'New file' link to dropdown on project page Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/projects/buttons/_dropdown.html.haml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 588909d257..c2bf3bf559 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.2.0 (unreleased) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. - [API] Add ability to fetch the commit ID of the last commit that actually touched a file + - Add "New file" link to dropdown on project page v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 4580c91269..bed2b16249 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -20,6 +20,10 @@ New snippet - if can?(current_user, :push_code, @project) %li.divider + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), title: 'New file' do + = icon('file fw') + New file %li = link_to new_namespace_project_branch_path(@project.namespace, @project) do = icon('code-fork fw') From 28f6fba97cb9753c43d109ef4f43439413d4eb69 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 10:16:09 +0100 Subject: [PATCH 340/443] Fix commits search for empty repository Signed-off-by: Dmitriy Zaporozhets --- lib/gitlab/project_search_results.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index c873ef3771..70de6a74e7 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -77,7 +77,11 @@ module Gitlab end def commits - project.repository.find_commits_by_message(query) + if project.empty_repo? || query.blank? + [] + else + project.repository.find_commits_by_message(query).compact + end end def limit_project_ids From fe34b745e71a3e4ebb18d77a186d24de23b50863 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 3 Nov 2015 11:05:54 +0100 Subject: [PATCH 341/443] Fix tests --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index a141a459ba..9963f76f99 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -123,18 +123,16 @@ module Ci config = YAML.dump({ before_script: ["pwd"], - build: { script: "build", type: "build", only: ["master", "deploy"] }, - rspec: { script: "rspec", type: type, only: ["master", "deploy"] }, + rspec: { script: "rspec", type: "test", only: ["master", "deploy"] }, staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, - production: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }, }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, 'fork') - expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) end end @@ -230,18 +228,16 @@ module Ci it "returns build except specified type" do config = YAML.dump({ before_script: ["pwd"], - build: { script: "build", type: "build", except: ["master", "deploy"] }, - rspec: { script: "rspec", type: type, except: ["master", "deploy", "test@fork"] }, - staging: { script: "deploy", type: "deploy", except: ["master", "deploy"] }, - production: { script: "deploy", type: "deploy", except: ["master", "deploy"] }, - production: { script: "deploy", type: "deploy", except: ["master@path", "deploy"] }, + rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, + staging: { script: "deploy", type: "deploy", except: ["master"] }, + production: { script: "deploy", type: "deploy", except: ["master@fork"] }, }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, 'fork') - expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref(type, "test").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) end end From 9479496f7584336788fc61121c096dfbcb3f6e4a Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 2 Nov 2015 16:56:44 +0200 Subject: [PATCH 342/443] Add added, modified and removed properties to commit object in webhook --- CHANGELOG | 1 + doc/web_hooks/web_hooks.md | 5 +++- lib/gitlab/push_data_builder.rb | 33 +++++++++++++++++++++-- spec/lib/gitlab/push_data_builder_spec.rb | 6 +++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 588909d257..78f72ccc1e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.2.0 (unreleased) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. - [API] Add ability to fetch the commit ID of the last commit that actually touched a file + - Add "added", "modified" and "removed" properties to commit object in webhook v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index ef99a69f60..7d838187a2 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -69,7 +69,10 @@ X-Gitlab-Event: Push Hook } } ], - "total_commits_count": 4 + "total_commits_count": 4, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] } ``` diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index d010ade704..fa068d5076 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -18,7 +18,10 @@ module Gitlab # homepage: String, # }, # commits: Array, - # total_commits_count: Fixnum + # total_commits_count: Fixnum, + # added: ["CHANGELOG"], + # modified: [], + # removed: ["tmp/file.txt"] # } # def build(project, user, oldrev, newrev, ref, commits = [], message = nil) @@ -33,6 +36,8 @@ module Gitlab commit_attrs = commits_limited.map(&:hook_attrs) type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" + + repo_changes = repo_changes(project, newrev, oldrev) # Hash to be passed as post_receive_data data = { object_kind: type, @@ -55,7 +60,10 @@ module Gitlab visibility_level: project.visibility_level }, commits: commit_attrs, - total_commits_count: commits_count + total_commits_count: commits_count, + added: repo_changes[:added], + modified: repo_changes[:modified], + removed: repo_changes[:removed] } data @@ -86,6 +94,27 @@ module Gitlab newrev end end + + def repo_changes(project, newrev, oldrev) + changes = { added: [], modified: [], removed: [] } + compare_result = CompareService.new. + execute(project, newrev, project, oldrev) + + if compare_result + compare_result.diffs.each do |diff| + case true + when diff.deleted_file + changes[:removed] << diff.old_path + when diff.renamed_file, diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path + end + end + end + + changes + end end end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 1b8ba7b4d4..0271074262 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -17,6 +17,9 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } + it { expect(data[:added]).to eq(["gitlab-grack"]) } + it { expect(data[:modified]).to eq([".gitmodules", "files/ruby/popen.rb", "files/ruby/regex.rb"]) } + it { expect(data[:removed]).to eq([]) } end describe :build do @@ -35,5 +38,8 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } + it { expect(data[:added]).to eq([]) } + it { expect(data[:modified]).to eq([]) } + it { expect(data[:removed]).to eq([]) } end end From 8a22b5bf544bd258b5cacca78b9f241849418d16 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Nov 2015 16:59:00 +0100 Subject: [PATCH 343/443] Remove inflector rule that makes commits uncountable Signed-off-by: Dmitriy Zaporozhets --- config/initializers/inflections.rb | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 5d46ece1e1..9e8b0131f8 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -8,24 +8,3 @@ # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end - -# Mark "commits" as uncountable. -# -# Without this change, the routes -# -# resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} -# resources :commits, only: [:show], constraints: {id: /.+/} -# -# would generate identical route helper methods (`project_commit_path`), resulting -# in one of them not getting a helper method at all. -# -# After this change, the helper methods are: -# -# project_commit_path(@project, @project.commit) -# # => "/gitlabhq/commit/bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a -# -# project_commits_path(@project, 'stable/README.md') -# # => "/gitlabhq/commits/stable/README.md" -ActiveSupport::Inflector.inflections do |inflect| - inflect.uncountable %w(commits) -end From 0df65909eff560f2d313c4034ed21a65d643a0c3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 3 Nov 2015 11:47:23 +0100 Subject: [PATCH 344/443] Added benchmark for User.all This benchmark exists to test if ordering has any noticeable impact in the test environment. --- spec/benchmarks/models/user_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb index 4cdba66939..1be7a8d3ed 100644 --- a/spec/benchmarks/models/user_spec.rb +++ b/spec/benchmarks/models/user_spec.rb @@ -1,6 +1,16 @@ require 'spec_helper' describe User, benchmark: true do + describe '.all' do + before do + 10.times { create(:user) } + end + + benchmark_subject { User.all.to_a } + + it { is_expected.to iterate_per_second(500) } + end + describe '.by_login' do before do %w{Alice Bob Eve}.each do |name| From 732f5380af5d688a152de9c5b4bb14ffb935dd2a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 3 Nov 2015 11:54:43 +0100 Subject: [PATCH 345/443] Only sort by IDs by default Sorting by both "created_at" and "id" in descending order is not needed as simply sorting by "id" in descending order will already sort rows from new to old. Depending on the query and data involved sorting twice can also introduce significant overhead. --- app/models/concerns/sortable.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 0ad2654867..913c747a1c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -8,12 +8,12 @@ module Sortable included do # By default all models should be ordered # by created_at field starting from newest - default_scope { order(created_at: :desc, id: :desc) } + default_scope { order(id: :desc) } - scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) } - scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) } - scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) } - scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) } + scope :order_created_desc, -> { reorder(created_at: :desc) } + scope :order_created_asc, -> { reorder(created_at: :asc) } + scope :order_updated_desc, -> { reorder(updated_at: :desc) } + scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_desc, -> { reorder(name: :desc) } end From a2f8f9ad3da777869ba702ebf3c5b85b18991c28 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 3 Nov 2015 11:56:04 +0100 Subject: [PATCH 346/443] Fixed User sorting specs The descriptions were not accurate and one particular spec seemingly expected the wrong User row to be returned. --- spec/models/user_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c71cfb3ebe..49e0bfdd2e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -663,24 +663,24 @@ describe User do @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 + it "sorts users by the recent sign-in time" do expect(User.sort('recent_sign_in').first).to eq(@user) end - it "sorts users as late_signed_in" do + it "sorts users by the oldest sign-in time" do expect(User.sort('oldest_sign_in').first).to eq(@user1) end - it "sorts users as recently_created" do + it "sorts users in descending order by their creation time" do expect(User.sort('created_desc').first).to eq(@user) end - it "sorts users as late_created" do + it "sorts users in ascending order by their creation time" do expect(User.sort('created_asc').first).to eq(@user1) end - it "sorts users by name when nil is passed" do - expect(User.sort(nil).first).to eq(@user) + it "sorts users by id in descending order when nil is passed" do + expect(User.sort(nil).first).to eq(@user1) end end From d23ffc832c465c873ad5a70117a57bf32f0b4735 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 12:00:27 +0100 Subject: [PATCH 347/443] Fix code that depends on incorrect inflector behavior Signed-off-by: Dmitriy Zaporozhets --- .../search/results/{_commits.html.haml => _commit.html.haml} | 2 +- spec/mailers/notify_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/views/search/results/{_commits.html.haml => _commit.html.haml} (90%) diff --git a/app/views/search/results/_commits.html.haml b/app/views/search/results/_commit.html.haml similarity index 90% rename from app/views/search/results/_commits.html.haml rename to app/views/search/results/_commit.html.haml index 7cff694350..4e6c3965dc 100644 --- a/app/views/search/results/_commits.html.haml +++ b/app/views/search/results/_commit.html.haml @@ -1,2 +1,2 @@ .search-result-row - = render 'projects/commits/commit', project: @project, commit: commits + = render 'projects/commits/commit', project: @project, commit: commit diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index cb67ec95d5..47863d5457 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -468,7 +468,7 @@ describe Notify do subject { Notify.note_commit_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'commits' + it_behaves_like 'an answer to an existing thread', 'commit' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ From 8d2758e02d634fd8518893f39dcc3359284e890f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Oct 2015 11:29:47 +0200 Subject: [PATCH 348/443] Cleanup stuck CI builds daily --- CHANGELOG | 1 + app/workers/stuck_ci_builds_worker.rb | 18 +++++++++ spec/workers/stuck_ci_builds_worker_spec.rb | 44 +++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 app/workers/stuck_ci_builds_worker.rb create mode 100644 spec/workers/stuck_ci_builds_worker_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 08c8507263..e4f235fc3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 8.1.0 - Fix duplicate repositories in GitHub import page (Stan Hu) - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Adds ability to create directories using the web editor (Ben Ford) + - Cleanup stuck CI builds v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb new file mode 100644 index 0000000000..ad02a3b16d --- /dev/null +++ b/app/workers/stuck_ci_builds_worker.rb @@ -0,0 +1,18 @@ +class StuckCiBuildsWorker + include Sidekiq::Worker + include Sidetiq::Schedulable + + BUILD_STUCK_TIMEOUT = 1.day + + recurrence { daily } + + def perform + Rails.logger.info 'Cleaning stuck builds' + + builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago) + builds.find_each(batch_size: 50).each do |build| + Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" + build.drop + end + end +end diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb new file mode 100644 index 0000000000..f9d87d9701 --- /dev/null +++ b/spec/workers/stuck_ci_builds_worker_spec.rb @@ -0,0 +1,44 @@ +require "spec_helper" + +describe StuckCiBuildsWorker do + let!(:build) { create :ci_build } + + subject do + build.reload + build.status + end + + %w(pending running).each do |status| + context "#{status} build" do + before do + build.update!(status: status) + end + + it 'gets dropped if it was updated over 2 days ago' do + build.update!(updated_at: 2.day.ago) + StuckCiBuildsWorker.new.perform + is_expected.to eq('failed') + end + + it "is still #{status}" do + build.update!(updated_at: 1.minute.ago) + StuckCiBuildsWorker.new.perform + is_expected.to eq(status) + end + end + end + + %w(success failed canceled).each do |status| + context "#{status} build" do + before do + build.update!(status: status) + end + + it "is still #{status}" do + build.update!(updated_at: 2.day.ago) + StuckCiBuildsWorker.new.perform + is_expected.to eq(status) + end + end + end +end From e3aed9121948c8124ddf128af3841986d69dafd5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 13:22:11 +0100 Subject: [PATCH 349/443] Better name for up-level links Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/layouts/nav/_group.html.haml | 4 ++-- app/views/layouts/nav/_group_settings.html.haml | 4 ++-- app/views/layouts/nav/_profile.html.haml | 4 ++-- app/views/layouts/nav/_project.html.haml | 8 ++++---- app/views/layouts/nav/_project_settings.html.haml | 4 ++-- features/steps/groups.rb | 2 +- features/steps/project/project.rb | 4 ++-- features/steps/shared/project_tab.rb | 2 +- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 08c8507263..f178db4626 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.2.0 (unreleased) - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Add "New file" link to dropdown on project page - Include commit logs in project search + - Rename "Back to" links to "Go to" because its not always a case it point to place user come from v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index eb35af22b9..319352876b 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,9 +1,9 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 8075fe32fb..c8411521f3 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,9 +1,9 @@ %ul.nav.nav-sidebar = nav_link do - = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@group), title: 'Go to group', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to group + Go to group %li.separate-item diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 5a47b8e6db..0f3a793e30 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,9 +1,9 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 53a913fe8f..20db2866d1 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,16 +1,16 @@ %ul.nav.nav-sidebar - if @project.group = nav_link do - = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@project.group), title: 'Go to group', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to group + Go to group - else = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 356ce09c3d..a59939ccd3 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,9 +1,9 @@ %ul.nav.nav-sidebar = nav_link do - = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do + = link_to project_path(@project), title: 'Go to project', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to project + Go to project %li.separate-item diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 69ddfa42c0..70388c18fc 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -6,7 +6,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include Select2Helper step 'I should see back to dashboard button' do - expect(page).to have_content 'Back to dashboard' + expect(page).to have_content 'Go to dashboard' end step 'gitlab user "Mike"' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index d76891d5bd..9ca7c8ebbc 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -124,11 +124,11 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see back to dashboard button' do - expect(page).to have_content 'Back to dashboard' + expect(page).to have_content 'Go to dashboard' end step 'I should see back to group button' do - expect(page).to have_content 'Back to group' + expect(page).to have_content 'Go to group' end step 'I click notifications drop down button' do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index c67e5e4a06..33ff7084e3 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -46,7 +46,7 @@ module SharedProjectTab step 'the active main tab should be Settings' do page.within '.nav-sidebar' do - expect(page).to have_content('Back to project') + expect(page).to have_content('Go to project') end end From ab129f7b235527d28068a3fd6ca1fc6eed5ebac5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 14:00:41 +0100 Subject: [PATCH 350/443] Improve profile page UI Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/profile.scss | 22 ++++++++++++++++++ app/views/users/show.html.haml | 27 ++++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b7391e5303..1d6ca0dfc1 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -53,3 +53,25 @@ float: right; font-size: 12px; } + +.profile-link-holder { + display: inline; + + &:after { + content: "\00B7"; + padding: 0px 6px; + font-weight: bold; + } + + &:last-child { + &:after { + content: ""; + padding: 0; + } + } + + a { + color: $blue-dark; + text-decoration: none; + } +} diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 4ea4a1f92c..3766d8cd67 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -24,22 +24,27 @@ .cover-desc - unless @user.public_email.blank? - = link_to @user.public_email, "mailto:#{@user.public_email}" + .profile-link-holder + = link_to @user.public_email, "mailto:#{@user.public_email}" - unless @user.skype.blank? - · - = link_to "Skype", "skype:#{@user.skype}" + .profile-link-holder + = link_to "skype:#{@user.skype}", title: "Skype" do + = icon('skype') - unless @user.linkedin.blank? - · - = link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}" + .profile-link-holder + = link_to "http://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = icon('linkedin-square') - unless @user.twitter.blank? - · - = link_to "Twitter", "http://www.twitter.com/#{@user.twitter}" + .profile-link-holder + = link_to "http://www.twitter.com/#{@user.twitter}", title: "Twitter" do + = icon('twitter-square') - unless @user.website_url.blank? - · - = link_to @user.short_website_url, @user.full_website_url + .profile-link-holder + = link_to @user.short_website_url, @user.full_website_url - unless @user.location.blank? - · - = @user.location + .profile-link-holder + = icon('map-marker') + = @user.location .cover-controls From 4d7f00fdeae074ae26f2629c78064d6eaa21ace1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 15:32:40 +0100 Subject: [PATCH 351/443] Apply new design for user profile page Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/assets/javascripts/calendar.js.coffee | 2 +- app/assets/stylesheets/framework/common.scss | 10 ++++ app/assets/stylesheets/pages/profile.scss | 4 ++ app/views/users/_projects.html.haml | 13 ---- app/views/users/show.html.haml | 62 ++++++++++++++------ 6 files changed, 59 insertions(+), 33 deletions(-) delete mode 100644 app/views/users/_projects.html.haml diff --git a/CHANGELOG b/CHANGELOG index ae3613cc33..4c43a377fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.2.0 (unreleased) - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Add "New file" link to dropdown on project page - Include commit logs in project search + - New design for user profile page v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 4c4bc3d66e..2b1e20d322 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -25,7 +25,7 @@ class @Calendar 30 ] legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 80 + cellSize: $('.user-calendar').width() / 76 onClick: (date, count) -> formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index e1a1793be9..3d0b71e066 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -387,6 +387,16 @@ table { } } +.center-middle-menu { + @include nav-menu; + text-align: center; + margin: -$gl-padding; + height: auto; + margin-top: 0; + margin-bottom: 0; + border-bottom: 1px solid $border-color; +} + .dropzone .dz-preview .dz-progress { border-color: $border-color !important; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 1d6ca0dfc1..bc1ad21305 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -75,3 +75,7 @@ text-decoration: none; } } + +.cal-heatmap-container { + margin: 0 auto; +} diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml deleted file mode 100644 index a126a858ea..0000000000 --- a/app/views/users/_projects.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? - .panel.panel-default.contributed-projects - .panel-heading Projects contributed to - = render 'shared/projects/list', - projects: contributed_projects.sort_by(&:star_count).reverse, - projects_limit: 5, stars: true, avatar: false - -- if local_assigns.has_key?(:projects) && projects.present? - .panel.panel-default - .panel-heading Personal projects - = render 'shared/projects/list', - projects: projects.sort_by(&:star_count).reverse, - projects_limit: 10, stars: true, avatar: false diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3766d8cd67..e22d93aae8 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -52,7 +52,7 @@ = link_to profile_path, class: 'btn btn-gray' do = icon('pencil') - elsif current_user - .report-abuse + %span.report-abuse - if @user.abuse_report %button.btn.btn-danger{ title: 'Already reported for abuse', data: { toggle: 'tooltip', placement: 'left', container: 'body' }} @@ -61,6 +61,10 @@ = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do = icon('exclamation-circle') + - if current_user +   + = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do + = icon('rss') .gray-content-block.second-block .user-calendar @@ -69,27 +73,47 @@ .user-calendar-activities -.row.prepend-top-20 - %section.col-md-7 - - if @groups.any? - .prepend-top-20 - %h4 Groups - = render 'groups', groups: @groups - %hr - - %h4 - User Activity - - - if current_user - %span.rss-icon.pull-right - = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do - %strong - %i.fa.fa-rss +%ul.nav.center-middle-menu + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @groups.any? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Groups + - if @contributed_projects.present? + %li + = link_to "#contributed", 'data-toggle' => 'tab' do + Contributed projects + - if @projects.present? + %li + = link_to "#personal", 'data-toggle' => 'tab' do + Personal projects +.tab-content + .tab-pane.active#activity .content_list = spinner - %aside.col-md-5 - = render 'projects', projects: @projects, contributed_projects: @contributed_projects + + - if @groups.any? + .tab-pane#groups + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group + + - if @contributed_projects.present? + .tab-pane#contributed + .contributed-projects + = render 'shared/projects/list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false + + - if @projects.present? + .tab-pane#personal + .personal-projects + = render 'shared/projects/list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false :coffeescript $(".user-calendar").load("#{user_calendar_path}") From 00ac792cfba52dffbbfd1bf304059fcd63d7e688 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 15:44:52 +0100 Subject: [PATCH 352/443] Fix clipboard button overflow Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/buttons.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0402441958..fe56266284 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -180,3 +180,7 @@ } } } + +.btn-clipboard { + border: none; +} From e0edc01d1477d463d2ecf8137c966859d32c1302 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 16:39:09 +0100 Subject: [PATCH 353/443] Fix tests Signed-off-by: Dmitriy Zaporozhets --- features/profile/profile.feature | 1 + features/steps/profile/profile.rb | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/features/profile/profile.feature b/features/profile/profile.feature index 27c0bde364..168d9d30b5 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -7,6 +7,7 @@ Feature: Profile Given I visit profile page Then I should see my profile info + @javascript Scenario: I can see groups I belong to Given I have group with projects When I visit profile page diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 8cf24705a5..40b2aa7c35 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -59,7 +59,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I should not see the "Remove avatar" button' do expect(page).not_to have_link("Remove avatar") end - + step 'I should see the gravatar host link' do expect(page).to have_link("gravatar.com") end @@ -159,10 +159,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see my user page' do - expect(page).to have_content "User Activity" - - page.within '.navbar-gitlab' do + page.within ".cover-block" do expect(page).to have_content current_user.name + expect(page).to have_content current_user.username end end @@ -176,7 +175,13 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see groups I belong to' do - expect(page).to have_css('.profile-groups-avatars', visible: true) + page.within ".content" do + click_link "Groups" + end + + page.within "#groups" do + expect(page).to have_content @group.name + end end step 'I click on new application button' do From ef6f6f80ec6242790d54a57445fd7e4915b36ed1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Nov 2015 16:42:02 +0100 Subject: [PATCH 354/443] Add extra padding between user description and links on profile page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/blocks.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 5949a0fd5a..8917c53b1f 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -100,7 +100,7 @@ } .cover-desc { - padding: 0 $gl-padding; + padding: 0 $gl-padding 3px; color: $gl-text-color; } From 700909d56726e23c500a2b55e2e68d14cbe3a161 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 2 Nov 2015 15:33:56 -0500 Subject: [PATCH 355/443] Bump stamp to ~> 0.6.0 Closes #2801 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bb31c147b2..df26063309 100644 --- a/Gemfile +++ b/Gemfile @@ -63,7 +63,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Format dates and times # based on human-friendly examples -gem "stamp", '~> 0.5.0' +gem "stamp", '~> 0.6.0' # Enumeration fields gem 'enumerize', '~> 0.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 65abc45ff1..030191fb80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -689,7 +689,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) - stamp (0.5.0) + stamp (0.6.0) state_machine (1.2.0) stringex (2.5.2) systemu (2.6.5) @@ -909,7 +909,7 @@ DEPENDENCIES spring-commands-spinach (~> 1.0.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 2.12.3) - stamp (~> 0.5.0) + stamp (~> 0.6.0) state_machine (~> 1.2.0) task_list (~> 1.0.2) teaspoon (~> 1.0.0) From 4773d98e835dd14bd73e7bde0d5bcf4754355976 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Nov 2015 17:58:12 +0100 Subject: [PATCH 356/443] Add Facebook authentication --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 3 + .../images/auth_buttons/facebook_64.png | Bin 0 -> 2970 bytes app/helpers/auth_helper.rb | 2 +- config/gitlab.yml.example | 29 +++--- doc/integration/facebook.md | 96 ++++++++++++++++++ doc/integration/facebook_api_keys.png | Bin 0 -> 125921 bytes doc/integration/facebook_app_settings.png | Bin 0 -> 134387 bytes doc/integration/facebook_website_url.png | Bin 0 -> 42292 bytes doc/integration/omniauth.md | 3 +- 11 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 app/assets/images/auth_buttons/facebook_64.png create mode 100644 doc/integration/facebook.md create mode 100644 doc/integration/facebook_api_keys.png create mode 100644 doc/integration/facebook_app_settings.png create mode 100644 doc/integration/facebook_website_url.png diff --git a/CHANGELOG b/CHANGELOG index 16055208db..0ec6030b13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.2.0 (unreleased) v 8.1.3 - Spread out runner contacted_at updates - New design for user profile page + - Add Facebook authentication v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) diff --git a/Gemfile b/Gemfile index bb31c147b2..8379e5eea1 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.1.3' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 65abc45ff1..c9ba4959dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -423,6 +423,8 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) + omniauth-facebook (3.0.0) + omniauth-oauth2 (~> 1.2) omniauth-github (1.1.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) @@ -859,6 +861,7 @@ DEPENDENCIES octokit (~> 3.7.0) omniauth (~> 1.2.2) omniauth-bitbucket (~> 0.0.2) + omniauth-facebook (~> 3.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.2.0) diff --git a/app/assets/images/auth_buttons/facebook_64.png b/app/assets/images/auth_buttons/facebook_64.png new file mode 100644 index 0000000000000000000000000000000000000000..1f1a80d7368ceb08b3e47d688f9e039b41ecfc0e GIT binary patch literal 2970 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

    Xgd&ZEA;!Z@-keQ(hUxt8h_2iO(!fO}dh` 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

    + From 2e5d32e07a75b98e57a0476a4cdb7a1ec41cadda Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Sun, 8 Nov 2015 12:15:58 -0500 Subject: [PATCH 407/443] Switch to state_machines-activerecord (Fixes: #3374) --- Gemfile | 2 +- config/initializers/state_machine_patch.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 0bac897816..ee8b2a7a51 100644 --- a/Gemfile +++ b/Gemfile @@ -113,7 +113,7 @@ group :unicorn do end # State machine -gem "state_machine", '~> 1.2.0' +gem "state_machines-activerecord", '~> 0.3.0' # Run events after state machine commits gem 'after_commit_queue' diff --git a/config/initializers/state_machine_patch.rb b/config/initializers/state_machine_patch.rb index 72d010fa5d..51f0579436 100644 --- a/config/initializers/state_machine_patch.rb +++ b/config/initializers/state_machine_patch.rb @@ -1,6 +1,6 @@ # This is a patch to address the issue in https://github.com/pluginaweek/state_machine/issues/251 # where gem 'state_machine' was not working for Rails 4.1 -module StateMachine +module StateMachines module Integrations module ActiveModel public :around_validation From ac5b6c3b50b5220d3fc800508267b320fa7cf78b Mon Sep 17 00:00:00 2001 From: Christophe Poulette Date: Sun, 8 Nov 2015 19:29:22 +0100 Subject: [PATCH 408/443] Apply new design for project graphs page --- CHANGELOG | 1 + app/views/projects/graphs/_head.html.haml | 2 +- app/views/projects/graphs/ci.html.haml | 3 +++ app/views/projects/graphs/commits.html.haml | 10 +++++++--- app/views/projects/graphs/show.html.haml | 8 ++++++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3a75f50e9a..6f6bc8a745 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.2.0 (unreleased) - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from - Allow groups to appear in the search results if the group owner allows it + - New design for project graphs page v 8.1.3 - Spread out runner contacted_at updates diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index bbfaf422a8..e0d06a14bf 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs +%ul.center-top-menu = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 4f69cc64f7..b2dfe97938 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,6 +1,9 @@ - page_title "Continuous Integration", "Graphs" = render "header_title" = render 'head' +.gray-content-block + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs #charts.ci-charts = render 'projects/graphs/ci/builds' = render 'projects/graphs/ci/build_times' diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 112be875b6..a21d744865 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,8 +1,12 @@ - page_title "Commits", "Graphs" = render "header_title" -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' -= render 'head' += reder 'head' + +.gray-content-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs %p.lead Commit statistics for diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index bd342911e4..6bbf15d05a 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,9 +1,13 @@ - page_title "Contributors", "Graphs" = render "header_title" -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs' = render 'head' +.gray-content-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .loading-graph .center %h3.page-title From b68a2e6f251cd0af7179eba5ed058fcaab009377 Mon Sep 17 00:00:00 2001 From: Christophe Poulette Date: Sun, 8 Nov 2015 20:15:03 +0100 Subject: [PATCH 409/443] Fix typo --- app/views/projects/graphs/commits.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index a21d744865..838d3e4061 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,6 +1,6 @@ - page_title "Commits", "Graphs" = render "header_title" -= reder 'head' += redner 'head' .gray-content-block .tree-ref-holder From 4ac26c6463b53e2a02a43835e6a919e2bc023fb7 Mon Sep 17 00:00:00 2001 From: Christophe Poulette Date: Sun, 8 Nov 2015 20:31:55 +0100 Subject: [PATCH 410/443] Fix typo. --- app/views/projects/graphs/commits.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 838d3e4061..4e0c3e5b3d 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,6 +1,6 @@ - page_title "Commits", "Graphs" = render "header_title" -= redner 'head' += render 'head' .gray-content-block .tree-ref-holder From 4fda9ef8a522352886fad08524b4ad4054a7eb60 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 5 Nov 2015 16:22:37 +0100 Subject: [PATCH 411/443] Fix tests --- spec/factories/ci/projects.rb | 2 ++ spec/models/application_setting_spec.rb | 28 +++++++++++++++++-- .../ci/register_build_service_spec.rb | 4 +++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb index 111e1a8281..1183a19035 100644 --- a/spec/factories/ci/projects.rb +++ b/spec/factories/ci/projects.rb @@ -33,6 +33,8 @@ FactoryGirl.define do gl_project factory: :empty_project + shared_runners_enabled false + factory :ci_project do token 'iPWx6WM4lhHNedGfBpPJNP' end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index de0b2ef4cd..f01fe8bd39 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -28,11 +28,11 @@ require 'spec_helper' describe ApplicationSetting, models: true do - it { expect(ApplicationSetting.create_from_defaults).to be_valid } + let(:setting) { ApplicationSetting.create_from_defaults } + + it { expect(setting).to be_valid } context 'restricted signup domains' do - let(:setting) { ApplicationSetting.create_from_defaults } - it 'set single domain' do setting.restricted_signup_domains_raw = 'example.com' expect(setting.restricted_signup_domains).to eq(['example.com']) @@ -53,4 +53,26 @@ describe ApplicationSetting, models: true do expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) end end + + context 'shared runners' do + let(:gl_project) { create(:empty_project) } + + before do + allow_any_instance_of(Project).to receive(:current_application_settings).and_return(setting) + end + + subject { gl_project.ensure_gitlab_ci_project.shared_runners_enabled } + + context 'enabled' do + before { setting.update_attributes(shared_runners_enabled: true) } + + it { is_expected.to be_truthy } + end + + context 'disabled' do + before { setting.update_attributes(shared_runners_enabled: false) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 781764627a..b370dfbe11 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -70,6 +70,10 @@ module Ci end context 'disallow shared runners' do + before do + gl_project.gitlab_ci_project.update(shared_runners_enabled: false) + end + context 'shared runner' do let(:build) { service.execute(shared_runner) } From 3727c9cab2ab48ca6aa2f5cfa3cd3126a02002b1 Mon Sep 17 00:00:00 2001 From: Sullivan SENECHAL Date: Wed, 21 Oct 2015 10:40:05 +0200 Subject: [PATCH 412/443] Add GitLabCI code coverage regex sample for PHPUnit --- app/views/projects/ci_settings/_form.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml index 20bdccc902..ee6b8885e2 100644 --- a/app/views/projects/ci_settings/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -103,8 +103,9 @@ %li pytest-cov (Python) - %code \d+\%\s*$ - - + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% %fieldset %legend Advanced settings From 0fb85939da482e50a7337d8762fb26e75bb16ce5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 9 Nov 2015 14:12:05 +0100 Subject: [PATCH 413/443] Fix incoming email config defaults --- CHANGELOG | 1 + config/initializers/1_settings.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2201221116..4951ae172c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.2.0 (unreleased) - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from - Allow groups to appear in the search results if the group owner allows it + - Fix incoming email config defaults v 8.1.3 - Spread out runner contacted_at updates diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 64d73d7232..8192d727f2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -193,8 +193,8 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_c Settings['incoming_email'] ||= Settingslogic.new({}) Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil? -Settings.incoming_email['ssl'] = 143 if Settings.incoming_email['ssl'].nil? -Settings.incoming_email['start_tls'] = 143 if Settings.incoming_email['start_tls'].nil? +Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].nil? +Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil? Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? # From dec3e4ce64df5f71a7cba7734cada1baa79242cd Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 4 Nov 2015 19:13:19 +0100 Subject: [PATCH 414/443] Added Sherlock, a custom profiling tool for GitLab Sherlock will be a new GitLab specific tool for measuring the performance of Rails requests (and SideKiq jobs at some point). Some of the things that are currently tracked: * SQL queries along with their timings, backtraces and query plans (using "EXPLAIN ANALYZE" for PostgreSQL and regular "EXPLAIN" for MySQL) * Timings of application files (including views) on a per line basis * Some meta data such as the request method, path, total duration, etc More tracking (e.g. Rugged or gitlab-shell timings) might be added in the future. Sherlock will replace any existing tools we have used so far (e.g. active_record_query_trace and rack-mini-profiler), hence the corresponding Gems have been removed from the Gemfile. Sherlock can be enabled by starting Rails as following: ENABLE_SHERLOCK=1 bundle exec rails s Recorded transactions can be found at `/sherlock/transactions`. --- Gemfile | 4 +- Gemfile.lock | 11 +-- app/assets/stylesheets/pages/sherlock.scss | 33 +++++++ .../sherlock/application_controller.rb | 12 +++ .../sherlock/file_samples_controller.rb | 7 ++ .../sherlock/queries_controller.rb | 7 ++ .../sherlock/transactions_controller.rb | 19 ++++ .../sherlock/file_samples/show.html.haml | 55 +++++++++++ .../sherlock/queries/_backtrace.html.haml | 27 ++++++ app/views/sherlock/queries/_general.html.haml | 50 ++++++++++ app/views/sherlock/queries/show.html.haml | 26 +++++ .../transactions/_file_samples.html.haml | 22 +++++ .../sherlock/transactions/_general.html.haml | 33 +++++++ .../sherlock/transactions/_queries.html.haml | 24 +++++ .../sherlock/transactions/index.html.haml | 40 ++++++++ .../sherlock/transactions/show.html.haml | 36 +++++++ config/initializers/rack_profiler.rb | 10 -- config/initializers/sherlock.rb | 5 + config/locales/sherlock.en.yml | 36 +++++++ config/routes.rb | 13 +++ lib/gitlab/sherlock.rb | 20 ++++ lib/gitlab/sherlock/collection.rb | 42 ++++++++ lib/gitlab/sherlock/file_sample.rb | 27 ++++++ lib/gitlab/sherlock/line_profiler.rb | 60 ++++++++++++ lib/gitlab/sherlock/line_sample.rb | 20 ++++ lib/gitlab/sherlock/location.rb | 22 +++++ lib/gitlab/sherlock/middleware.rb | 36 +++++++ lib/gitlab/sherlock/query.rb | 96 +++++++++++++++++++ lib/gitlab/sherlock/transaction.rb | 85 ++++++++++++++++ 29 files changed, 855 insertions(+), 23 deletions(-) create mode 100644 app/assets/stylesheets/pages/sherlock.scss create mode 100644 app/controllers/sherlock/application_controller.rb create mode 100644 app/controllers/sherlock/file_samples_controller.rb create mode 100644 app/controllers/sherlock/queries_controller.rb create mode 100644 app/controllers/sherlock/transactions_controller.rb create mode 100644 app/views/sherlock/file_samples/show.html.haml create mode 100644 app/views/sherlock/queries/_backtrace.html.haml create mode 100644 app/views/sherlock/queries/_general.html.haml create mode 100644 app/views/sherlock/queries/show.html.haml create mode 100644 app/views/sherlock/transactions/_file_samples.html.haml create mode 100644 app/views/sherlock/transactions/_general.html.haml create mode 100644 app/views/sherlock/transactions/_queries.html.haml create mode 100644 app/views/sherlock/transactions/index.html.haml create mode 100644 app/views/sherlock/transactions/show.html.haml delete mode 100644 config/initializers/rack_profiler.rb create mode 100644 config/initializers/sherlock.rb create mode 100644 config/locales/sherlock.en.yml create mode 100644 lib/gitlab/sherlock.rb create mode 100644 lib/gitlab/sherlock/collection.rb create mode 100644 lib/gitlab/sherlock/file_sample.rb create mode 100644 lib/gitlab/sherlock/line_profiler.rb create mode 100644 lib/gitlab/sherlock/line_sample.rb create mode 100644 lib/gitlab/sherlock/location.rb create mode 100644 lib/gitlab/sherlock/middleware.rb create mode 100644 lib/gitlab/sherlock/query.rb create mode 100644 lib/gitlab/sherlock/transaction.rb diff --git a/Gemfile b/Gemfile index 0bac897816..b0a7c9b945 100644 --- a/Gemfile +++ b/Gemfile @@ -215,11 +215,9 @@ group :development do gem "annotate", "~> 2.6.0" gem "letter_opener", '~> 1.1.2' gem 'quiet_assets', '~> 1.0.2' - gem 'rack-mini-profiler', '~> 0.9.0', require: false gem 'rerun', '~> 0.10.0' gem 'bullet', require: false - gem 'active_record_query_trace', require: false - gem 'rack-lineprof', platform: :mri + gem 'rblineprof', platform: :mri, require: false # Better errors handler gem 'better_errors', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index dce728baf1..c602d40671 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,7 +17,6 @@ GEM activesupport (= 4.1.12) builder (~> 3.1) erubis (~> 2.7.0) - active_record_query_trace (1.5) activemodel (4.1.12) activesupport (= 4.1.12) builder (~> 3.1) @@ -491,12 +490,6 @@ GEM rack-attack (4.3.0) rack rack-cors (0.4.0) - rack-lineprof (0.0.3) - rack (~> 1.5) - rblineprof (~> 0.3.6) - term-ansicolor (~> 1.3) - rack-mini-profiler (0.9.7) - rack (>= 1.1.3) rack-mount (0.8.3) rack (>= 1.0.0) rack-oauth2 (1.0.10) @@ -779,7 +772,6 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) ace-rails-ap (~> 2.0.1) - active_record_query_trace activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) @@ -878,11 +870,10 @@ DEPENDENCIES quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) rack-cors (~> 0.4.0) - rack-lineprof - rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) rails (= 4.1.12) raphael-rails (~> 2.1.2) + rblineprof rdoc (~> 3.6) redcarpet (~> 3.3.3) redis-rails (~> 4.0.0) diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss new file mode 100644 index 0000000000..92d84d9640 --- /dev/null +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -0,0 +1,33 @@ +table .sherlock-code { + max-width: 700px; +} + +.sherlock-code { + pre { + word-wrap: normal; + } + + pre code { + white-space: pre; + } +} + +.sherlock-line-samples-table { + margin-bottom: 0px !important; + + thead tr th, + tbody tr td { + font-size: 13px !important; + text-align: right; + padding: 0px 10px !important; + } +} + +.sherlock-file-sample pre { + padding-top: 28px !important; +} + +.sherlock-line-samples-table .slow { + color: $red-light; + font-weight: bold; +} diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb new file mode 100644 index 0000000000..682ca5e382 --- /dev/null +++ b/app/controllers/sherlock/application_controller.rb @@ -0,0 +1,12 @@ +module Sherlock + class ApplicationController < ::ApplicationController + before_action :find_transaction + + def find_transaction + if params[:transaction_id] + @transaction = Gitlab::Sherlock.collection. + find_transaction(params[:transaction_id]) + end + end + end +end diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb new file mode 100644 index 0000000000..0c3bc10010 --- /dev/null +++ b/app/controllers/sherlock/file_samples_controller.rb @@ -0,0 +1,7 @@ +module Sherlock + class FileSamplesController < Sherlock::ApplicationController + def show + @file_sample = @transaction.find_file_sample(params[:id]) + end + end +end diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb new file mode 100644 index 0000000000..63b26aab1a --- /dev/null +++ b/app/controllers/sherlock/queries_controller.rb @@ -0,0 +1,7 @@ +module Sherlock + class QueriesController < Sherlock::ApplicationController + def show + @query = @transaction.find_query(params[:id]) + end + end +end diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb new file mode 100644 index 0000000000..ccc739da87 --- /dev/null +++ b/app/controllers/sherlock/transactions_controller.rb @@ -0,0 +1,19 @@ +module Sherlock + class TransactionsController < Sherlock::ApplicationController + def index + @transactions = Gitlab::Sherlock.collection.newest_first + end + + def show + @transaction = Gitlab::Sherlock.collection.find_transaction(params[:id]) + + render_404 unless @transaction + end + + def destroy_all + Gitlab::Sherlock.collection.clear + + redirect_to(:back) + end + end +end diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml new file mode 100644 index 0000000000..cfd11e45b6 --- /dev/null +++ b/app/views/sherlock/file_samples/show.html.haml @@ -0,0 +1,55 @@ +- page_title t('sherlock.title'), t('sherlock.transaction'), + t('sherlock.file_sample') + +- header_title t('sherlock.title'), sherlock_transactions_path + +.gray-content-block + .pull-right + = link_to(sherlock_transaction_path(@transaction), class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.transaction') + .oneline + = t('sherlock.file_sample') + = @file_sample.id + +.prepend-top-default + %p + %span.light + #{t('sherlock.time')}: + %strong + = @file_sample.duration.round(2) + = t('sherlock.milliseconds') + %p + %span.light + #{t('sherlock.events')}: + %strong + = @file_sample.events + +%article.file-holder + .file-title + %i.fa.fa-file-text-o.fa-fw + %strong + = @file_sample.file + .code.file-content.js-syntax-highlight + .line-numbers + %table.sherlock-line-samples-table + %thead + %tr + %th= t('sherlock.line_capitalized') + %th= t('sherlock.events') + %th= t('sherlock.time') + %th= t('sherlock.percent') + %tbody + - @file_sample.line_samples.each_with_index do |sample, index| + %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''} + %td= index + 1 + %td= sample.events + %td + = sample.duration.round(2) + = t('sherlock.milliseconds') + %td + = sample.percentage_of(@file_sample.duration).round + = t('sherlock.percent') + + .sherlock-file-sample + = highlight(@file_sample.file, @file_sample.source) diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml new file mode 100644 index 0000000000..5c9294c0ab --- /dev/null +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -0,0 +1,27 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.application_backtrace') + %ul.well-list + - @query.application_backtrace.each do |location| + %li + = location.path + %small.light + = t('sherlock.line') + = location.line + + .panel.panel-default + .panel-heading + %strong + = t('sherlock.full_backtrace') + %ul.well-list + - @query.backtrace.each do |location| + %li + - if location.application? + %strong= location.path + - else + = location.path + %small.light + = t('sherlock.line') + = location.line diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml new file mode 100644 index 0000000000..549b47430e --- /dev/null +++ b/app/views/sherlock/queries/_general.html.haml @@ -0,0 +1,50 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.general') + %ul.well-list + %li + %span.light + #{t('sherlock.time')}: + %strong + = @query.duration.round(4) + = t('sherlock.milliseconds') + %li + %span.light + #{t('sherlock.origin')}: + %strong + = @query.last_application_frame.path + %small.light + = t('sherlock.line') + = @query.last_application_frame.line + + .panel.panel-default + .panel-heading + .pull-right + %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %i.fa.fa-clipboard + %pre.hidden + = @query.formatted_query + %strong + = t('sherlock.query') + %ul.well-list + %li + .code.js-syntax-highlight.sherlock-code + :preserve + #{highlight("#{@query.id}.sql", @query.formatted_query)} + + .panel.panel-default + .panel-heading + .pull-right + %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %i.fa.fa-clipboard + %pre.hidden + = @query.explain + %strong + = t('sherlock.query_plan') + %ul.well-list + %li + .code.js-syntax-highlight.sherlock-code + %pre + %code= @query.explain diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml new file mode 100644 index 0000000000..4a84348ac8 --- /dev/null +++ b/app/views/sherlock/queries/show.html.haml @@ -0,0 +1,26 @@ +- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') +- header_title t('sherlock.title'), sherlock_transactions_path + +%ul.center-top-menu + %li.active + %a(href="#tab-general" data-toggle="tab") + = t('sherlock.general') + %li + %a(href="#tab-backtrace" data-toggle="tab") + = t('sherlock.backtrace') + +.gray-content-block + .pull-right + = link_to(sherlock_transaction_path(@transaction), class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.transaction') + .oneline + = t('sherlock.query') + = @query.id + +.tab-content + .tab-pane.active#tab-general + = render(partial: 'general') + + .tab-pane#tab-backtrace + = render(partial: 'backtrace') diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml new file mode 100644 index 0000000000..0afdbc8dff --- /dev/null +++ b/app/views/sherlock/transactions/_file_samples.html.haml @@ -0,0 +1,22 @@ +- if @transaction.file_samples.empty? + .nothing-here-block + = t('sherlock.no_file_samples') +- else + .table-holder + %table.table + %thead + %tr + %th= t('sherlock.time_inclusive') + %th= t('sherlock.path') + %th + %tbody + - @transaction.sorted_file_samples.each do |sample| + %tr + %td + = sample.duration.round(2) + = t('sherlock.milliseconds') + %td= sample.relative_path + %td + = link_to(t('sherlock.view'), + sherlock_transaction_file_sample_path(@transaction, sample), + class: 'btn btn-xs') diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml new file mode 100644 index 0000000000..4287a0c320 --- /dev/null +++ b/app/views/sherlock/transactions/_general.html.haml @@ -0,0 +1,33 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.general') + %ul.well-list + %li + %span.light + #{t('sherlock.id')}: + %strong + = @transaction.id + %li + %span.light + #{t('sherlock.type')}: + %strong + = @transaction.type + %li + %span.light + #{t('sherlock.path')}: + %strong + = @transaction.path + %li + %span.light + #{t('sherlock.time')}: + %strong + = @transaction.duration.round(2) + = t('sherlock.seconds') + %li + %span.light + #{t('sherlock.finished_at')}: + %strong + = time_ago_in_words(@transaction.finished_at) + = t('sherlock.ago') diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml new file mode 100644 index 0000000000..b7e0162e80 --- /dev/null +++ b/app/views/sherlock/transactions/_queries.html.haml @@ -0,0 +1,24 @@ +- if @transaction.queries.empty? + .nothing-here-block + = t('sherlock.no_queries') +- else + .table-holder + %table.table#sherlock-queries + %thead + %tr + %th= t('sherlock.time') + %th= t('sherlock.query') + %td + %tbody + - @transaction.sorted_queries.each do |query| + %tr + %td + = query.duration.round(2) + = t('sherlock.milliseconds') + %td + .code.js-syntax-highlight.sherlock-code + = highlight("#{query.id}.sql", query.formatted_query) + %td + = link_to(t('sherlock.view'), + sherlock_transaction_query_path(@transaction, query), + class: 'btn btn-xs') diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml new file mode 100644 index 0000000000..fb31131ba8 --- /dev/null +++ b/app/views/sherlock/transactions/index.html.haml @@ -0,0 +1,40 @@ +- page_title t('sherlock.title') +- header_title t('sherlock.title'), sherlock_transactions_path + +.gray-content-block + .pull-right + = link_to(destroy_all_sherlock_transactions_path, + class: 'btn btn-danger', + method: :delete) do + %i.fa.fa-trash + = t('sherlock.delete_all_transactions') + .oneline= t('sherlock.introduction') + +- if @transactions.empty? + .nothing-here-block= t('sherlock.no_transactions') +- else + .table-holder + %table.table + %thead + %tr + %th= t('sherlock.type') + %th= t('sherlock.path') + %th= t('sherlock.time') + %th= t('sherlock.queries') + %th= t('sherlock.finished_at') + %th + %tbody + - @transactions.each do |trans| + %tr + %td= trans.type + %td= trans.path + %td + = trans.duration.round(2) + = t('sherlock.seconds') + %td= trans.queries.length + %td + = time_ago_in_words(trans.finished_at) + = t('sherlock.ago') + %td + = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do + = t('sherlock.view') diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml new file mode 100644 index 0000000000..3c8ffb0664 --- /dev/null +++ b/app/views/sherlock/transactions/show.html.haml @@ -0,0 +1,36 @@ +- page_title t('sherlock.title'), t('sherlock.transaction') +- header_title t('sherlock.title'), sherlock_transactions_path + +%ul.center-top-menu + %li.active + %a(href="#tab-general" data-toggle="tab") + = t('sherlock.general') + %li + %a(href="#tab-queries" data-toggle="tab") + = t('sherlock.queries') + %span.badge + #{@transaction.queries.length} + %li + %a(href="#tab-file-samples" data-toggle="tab") + = t('sherlock.file_samples') + %span.badge + #{@transaction.file_samples.length} + +.gray-content-block + .pull-right + = link_to(sherlock_transactions_path, class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.all_transactions') + .oneline + = t('sherlock.transaction') + = @transaction.id + +.tab-content + .tab-pane.active#tab-general + = render(partial: 'general') + + .tab-pane#tab-queries + = render(partial: 'queries') + + .tab-pane#tab-file-samples + = render(partial: 'file_samples') diff --git a/config/initializers/rack_profiler.rb b/config/initializers/rack_profiler.rb deleted file mode 100644 index 7710eeac45..0000000000 --- a/config/initializers/rack_profiler.rb +++ /dev/null @@ -1,10 +0,0 @@ -if Rails.env.development? - require 'rack-mini-profiler' - - # initialization is skipped so trigger it - Rack::MiniProfilerRails.initialize!(Gitlab::Application) - - Rack::MiniProfiler.config.position = 'right' - Rack::MiniProfiler.config.start_hidden = false - Rack::MiniProfiler.config.skip_paths << '/teaspoon' -end diff --git a/config/initializers/sherlock.rb b/config/initializers/sherlock.rb new file mode 100644 index 0000000000..42b0d78c85 --- /dev/null +++ b/config/initializers/sherlock.rb @@ -0,0 +1,5 @@ +if Gitlab::Sherlock.enabled? + Gitlab::Application.configure do |config| + config.middleware.use(Gitlab::Sherlock::Middleware) + end +end diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml new file mode 100644 index 0000000000..5c146b172b --- /dev/null +++ b/config/locales/sherlock.en.yml @@ -0,0 +1,36 @@ +en: + sherlock: + title: Sherlock + delete_all_transactions: Delete All Transactions + introduction: > + Below is a list of all transactions recorded by Sherlock. Requests to + Sherlock's own routes are ignored. + no_transactions: No transactions to show + no_queries: No queries to show + no_file_samples: No file samples to show + all_transactions: All Transactions + transaction: Transaction + query: Query + file_sample: File Sample + type: Type + path: Path + time: Time + queries: Queries + finished_at: Finished at + ago: ago + view: View + seconds: seconds + milliseconds: ms + general: General + id: ID + time_inclusive: Time (inclusive) + backtrace: Backtrace + application_backtrace: Application Backtrace + full_backtrace: Full Backtrace + origin: Origin + line: line + line_capitalized: Line + copy_to_clipboard: Copy to clipboard + query_plan: Query Plan + events: Events + percent: '%' diff --git a/config/routes.rb b/config/routes.rb index 990a00e3d0..7d8a546a64 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,19 @@ require 'sidekiq/web' require 'api/api' Gitlab::Application.routes.draw do + if Gitlab::Sherlock.enabled? + namespace :sherlock do + resources :transactions, only: [:index, :show] do + resources :queries, only: [:show] + resources :file_samples, only: [:show] + + collection do + delete :destroy_all + end + end + end + end + namespace :ci do # CI API Ci::API::API.logger Rails.logger diff --git a/lib/gitlab/sherlock.rb b/lib/gitlab/sherlock.rb new file mode 100644 index 0000000000..c4b35b24ce --- /dev/null +++ b/lib/gitlab/sherlock.rb @@ -0,0 +1,20 @@ +require 'securerandom' +require 'rblineprof' if RUBY_ENGINE == 'ruby' + +module Gitlab + module Sherlock + @collection = Collection.new + + class << self + attr_reader :collection + end + + def self.enabled? + Rails.env.development? && !!ENV['ENABLE_SHERLOCK'] + end + + def self.enable_line_profiler? + RUBY_ENGINE == 'ruby' + end + end +end diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb new file mode 100644 index 0000000000..accdc6469b --- /dev/null +++ b/lib/gitlab/sherlock/collection.rb @@ -0,0 +1,42 @@ +module Gitlab + module Sherlock + class Collection + include Enumerable + + def initialize + @transactions = [] + @mutex = Mutex.new + end + + def add(transaction) + synchronize { @transactions << transaction } + end + + alias_method :<<, :add + + def each(&block) + synchronize { @transactions.each(&block) } + end + + def clear + synchronize { @transactions.clear } + end + + def empty? + synchronize { @transactions.empty? } + end + + def find_transaction(id) + find { |trans| trans.id == id } + end + + def newest_first + sort { |a, b| b.finished_at <=> a.finished_at } + end + + def synchronize(&block) + @mutex.synchronize(&block) + end + end + end +end diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb new file mode 100644 index 0000000000..7a220de9ab --- /dev/null +++ b/lib/gitlab/sherlock/file_sample.rb @@ -0,0 +1,27 @@ +module Gitlab + module Sherlock + class FileSample + attr_reader :id, :file, :line_samples, :events, :duration + + def initialize(file, line_samples, duration, events) + @id = SecureRandom.uuid + @file = file + @line_samples = line_samples + @duration = duration + @events = events + end + + def relative_path + @relative_path ||= @file.gsub(/^#{Rails.root.to_s}\/?/, '') + end + + def to_param + @id + end + + def source + @source ||= File.read(@file) + end + end + end +end diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb new file mode 100644 index 0000000000..a191b1e646 --- /dev/null +++ b/lib/gitlab/sherlock/line_profiler.rb @@ -0,0 +1,60 @@ +module Gitlab + module Sherlock + class LineProfiler + # The minimum amount of time that has to be spent in a file for it to be + # included in a list of samples. + MINIMUM_DURATION = 10.0 + + def profile(&block) + if RUBY_ENGINE == 'ruby' + profile_mri(&block) + else + raise NotImplementedError, + 'Line profiling is not supported on this platform' + end + end + + def profile_mri + retval = nil + samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield } + + file_samples = aggregate_rblineprof(samples) + + [retval, file_samples] + end + + # Returns an Array of file samples based on the output of rblineprof. + def aggregate_rblineprof(lineprof_stats) + samples = [] + + lineprof_stats.each do |(file, stats)| + source_lines = File.read(file).each_line.to_a + line_samples = [] + + total_duration = microsec_to_millisec(stats[0][0]) + total_events = stats[0][2] + + next if total_duration <= MINIMUM_DURATION + + stats[1..-1].each_with_index do |data, index| + next unless source_lines[index] + + duration = microsec_to_millisec(data[0]) + events = data[2] + + line_samples << LineSample.new(duration, events) + end + + samples << FileSample. + new(file, line_samples, total_duration, total_events) + end + + samples + end + + def microsec_to_millisec(microsec) + microsec / 1000.0 + end + end + end +end diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb new file mode 100644 index 0000000000..38df7a88e4 --- /dev/null +++ b/lib/gitlab/sherlock/line_sample.rb @@ -0,0 +1,20 @@ +module Gitlab + module Sherlock + class LineSample + attr_reader :duration, :events + + def initialize(duration, events) + @duration = duration + @events = events + end + + def percentage_of(total_duration) + (duration.to_f / total_duration) * 100.0 + end + + def majority_of?(total_duration) + percentage_of(total_duration) >= 30 + end + end + end +end diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb new file mode 100644 index 0000000000..8c0b77dce1 --- /dev/null +++ b/lib/gitlab/sherlock/location.rb @@ -0,0 +1,22 @@ +module Gitlab + module Sherlock + class Location + attr_reader :path, :line + + SHERLOCK_DIR = File.dirname(__FILE__) + + def self.from_ruby_location(location) + new(location.path, location.lineno) + end + + def initialize(path, line) + @path = path + @line = line + end + + def application? + @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR) + end + end + end +end diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb new file mode 100644 index 0000000000..fca7be858e --- /dev/null +++ b/lib/gitlab/sherlock/middleware.rb @@ -0,0 +1,36 @@ +module Gitlab + module Sherlock + # Rack middleware used for tracking request metrics. + class Middleware + CONTENT_TYPES = /text\/html|application\/json/i + + IGNORE_PATHS = %r{^/sherlock} + + def initialize(app) + @app = app + end + + def call(env) + if instrument?(env) + call_with_instrumentation(env) + else + @app.call(env) + end + end + + def call_with_instrumentation(env) + trans = Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI']) + retval = trans.run { @app.call(env) } + + Sherlock.collection.add(trans) + + retval + end + + def instrument?(env) + !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES && + env['REQUEST_URI'] !~ IGNORE_PATHS) + end + end + end +end diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb new file mode 100644 index 0000000000..af76e6fd2b --- /dev/null +++ b/lib/gitlab/sherlock/query.rb @@ -0,0 +1,96 @@ +module Gitlab + module Sherlock + class Query + attr_reader :id, :query, :started_at, :finished_at, :backtrace + + PREFIX_NEWLINE = / + \s+(FROM + |(LEFT|RIGHT)?INNER\s+JOIN + |(LEFT|RIGHT)?OUTER\s+JOIN + |WHERE + |AND + |GROUP\s+BY + |ORDER\s+BY + |LIMIT + |OFFSET)\s+ + /ix + + def self.new_with_bindings(query, bindings, started_at, finished_at) + bindings.each_with_index do |(column, value), index| + quoted_value = ActiveRecord::Base.connection.quote(value) + + query = query.gsub("$#{index + 1}", quoted_value) + end + + new(query, started_at, finished_at) + end + + def initialize(query, started_at, finished_at) + @id = SecureRandom.uuid + @query = query + @started_at = started_at + @finished_at = finished_at + @backtrace = caller_locations.map do |loc| + Location.from_ruby_location(loc) + end + + unless @query.end_with?(';') + @query += ';' + end + end + + def duration + @duration ||= (@finished_at - @started_at) * 1000.0 + end + + def to_param + @id + end + + def formatted_query + @formatted_query ||= format_sql(@query) + end + + def last_application_frame + @last_application_frame ||= @backtrace.find(&:application?) + end + + def application_backtrace + @application_backtrace ||= @backtrace.select(&:application?) + end + + def explain + unless @explain + ActiveRecord::Base.connection.transaction do + @explain = raw_explain(@query).values.flatten.join("\n") + + # Roll back any queries that mutate data so we don't mess up + # anything when running explain on an INSERT, UPDATE, DELETE, etc. + raise ActiveRecord::Rollback + end + end + + @explain + end + + private + + def raw_explain(query) + if Gitlab::Database.postgresql? + explain = "EXPLAIN ANALYZE #{query};" + else + explain = "EXPLAIN #{query};" + end + + ActiveRecord::Base.connection.execute(explain) + end + + def format_sql(query) + query.each_line. + map { |line| line.strip }. + join("\n"). + gsub(PREFIX_NEWLINE) { "\n#{$1} " } + end + end + end +end diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb new file mode 100644 index 0000000000..5cb3e86aa4 --- /dev/null +++ b/lib/gitlab/sherlock/transaction.rb @@ -0,0 +1,85 @@ +module Gitlab + module Sherlock + class Transaction + attr_reader :id, :type, :path, :queries, :file_samples, :started_at, + :finished_at + + def initialize(type, path) + @id = SecureRandom.uuid + @type = type + @path = path + @duration = 0 + @queries = [] + @file_samples = [] + @started_at = nil + @finished_at = nil + @thread = Thread.current + end + + def run + @started_at = Time.now + + subscriber = subscribe_to_active_record + + retval = profile_lines { yield } + + @finished_at = Time.now + + ActiveSupport::Notifications.unsubscribe(subscriber) + + retval + end + + def duration + @started_at && @finished_at ? @finished_at - @started_at : 0 + end + + def to_param + @id + end + + def sorted_queries + @queries.sort { |a, b| b.duration <=> a.duration } + end + + def sorted_file_samples + @file_samples.sort { |a, b| b.duration <=> a.duration } + end + + def find_query(id) + @queries.find { |query| query.id == id } + end + + def find_file_sample(id) + @file_samples.find { |sample| sample.id == id } + end + + def track_query(query, bindings, start, finish) + @queries << Query.new_with_bindings(query, bindings, start, finish) + end + + def profile_lines + retval = nil + + if Sherlock.enable_line_profiler? + retval, @file_samples = LineProfiler.new.profile { yield } + else + retval = yield + end + + retval + end + + def subscribe_to_active_record + ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data| + # In case somebody uses a multi-threaded server locally (e.g. Puma) we + # _only_ want to track queries that originate from the transaction + # thread. + next unless Thread.current == @thread + + track_query(data[:sql].strip, data[:binds], start, finish) + end + end + end + end +end From 265ef867fff165643784640d837579ce4fcc2207 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 5 Nov 2015 18:01:05 +0100 Subject: [PATCH 415/443] Added specs and source documentation for Sherlock --- lib/gitlab/sherlock/collection.rb | 7 + lib/gitlab/sherlock/file_sample.rb | 4 + lib/gitlab/sherlock/line_profiler.rb | 38 +++- lib/gitlab/sherlock/line_sample.rb | 16 ++ lib/gitlab/sherlock/location.rb | 4 + lib/gitlab/sherlock/middleware.rb | 7 +- lib/gitlab/sherlock/query.rb | 24 ++- lib/gitlab/sherlock/transaction.rb | 29 ++- spec/lib/gitlab/sherlock/collection_spec.rb | 82 +++++++++ spec/lib/gitlab/sherlock/file_sample_spec.rb | 54 ++++++ .../lib/gitlab/sherlock/line_profiler_spec.rb | 73 ++++++++ spec/lib/gitlab/sherlock/line_sample_spec.rb | 33 ++++ spec/lib/gitlab/sherlock/location_spec.rb | 40 +++++ spec/lib/gitlab/sherlock/middleware_spec.rb | 79 +++++++++ spec/lib/gitlab/sherlock/query_spec.rb | 113 ++++++++++++ spec/lib/gitlab/sherlock/transaction_spec.rb | 165 ++++++++++++++++++ 16 files changed, 757 insertions(+), 11 deletions(-) create mode 100644 spec/lib/gitlab/sherlock/collection_spec.rb create mode 100644 spec/lib/gitlab/sherlock/file_sample_spec.rb create mode 100644 spec/lib/gitlab/sherlock/line_profiler_spec.rb create mode 100644 spec/lib/gitlab/sherlock/line_sample_spec.rb create mode 100644 spec/lib/gitlab/sherlock/location_spec.rb create mode 100644 spec/lib/gitlab/sherlock/middleware_spec.rb create mode 100644 spec/lib/gitlab/sherlock/query_spec.rb create mode 100644 spec/lib/gitlab/sherlock/transaction_spec.rb diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb index accdc6469b..66bd625852 100644 --- a/lib/gitlab/sherlock/collection.rb +++ b/lib/gitlab/sherlock/collection.rb @@ -1,5 +1,10 @@ module Gitlab module Sherlock + # A collection of transactions recorded by Sherlock. + # + # Method calls for this class are synchronized using a mutex to allow + # sharing of a single Collection instance between threads (e.g. when using + # Puma as a webserver). class Collection include Enumerable @@ -34,6 +39,8 @@ module Gitlab sort { |a, b| b.finished_at <=> a.finished_at } end + private + def synchronize(&block) @mutex.synchronize(&block) end diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb index 7a220de9ab..8a3e1a5e5b 100644 --- a/lib/gitlab/sherlock/file_sample.rb +++ b/lib/gitlab/sherlock/file_sample.rb @@ -3,6 +3,10 @@ module Gitlab class FileSample attr_reader :id, :file, :line_samples, :events, :duration + # file - The full path to the file this sample belongs to. + # line_samples - An array of LineSample objects. + # duration - The total execution time in milliseconds. + # events - The total amount of events. def initialize(file, line_samples, duration, events) @id = SecureRandom.uuid @file = file diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb index a191b1e646..152749dcc3 100644 --- a/lib/gitlab/sherlock/line_profiler.rb +++ b/lib/gitlab/sherlock/line_profiler.rb @@ -1,12 +1,36 @@ module Gitlab module Sherlock + # Class for profiling code on a per line basis. + # + # The LineProfiler class can be used to profile code on per line basis + # without littering your code with Ruby implementation specific profiling + # methods. + # + # This profiler only includes samples taking longer than a given threshold + # and those that occur in the actual application (e.g. files from Gems are + # ignored). class LineProfiler # The minimum amount of time that has to be spent in a file for it to be # included in a list of samples. MINIMUM_DURATION = 10.0 + # Profiles the given block. + # + # Example: + # + # profiler = LineProfiler.new + # + # retval, samples = profiler.profile do + # "cats are amazing" + # end + # + # retval # => "cats are amazing" + # samples # => [#, ...] + # + # Returns an Array containing the block's return value and an Array of + # FileSample objects. def profile(&block) - if RUBY_ENGINE == 'ruby' + if mri? profile_mri(&block) else raise NotImplementedError, @@ -14,6 +38,7 @@ module Gitlab end end + # Profiles the given block using rblineprof (MRI only). def profile_mri retval = nil samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield } @@ -24,6 +49,11 @@ module Gitlab end # Returns an Array of file samples based on the output of rblineprof. + # + # lineprof_stats - A Hash containing rblineprof statistics on a per file + # basis. + # + # Returns an Array of FileSample objects. def aggregate_rblineprof(lineprof_stats) samples = [] @@ -52,9 +82,15 @@ module Gitlab samples end + private + def microsec_to_millisec(microsec) microsec / 1000.0 end + + def mri? + RUBY_ENGINE == 'ruby' + end end end end diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb index 38df7a88e4..eb1948eb6d 100644 --- a/lib/gitlab/sherlock/line_sample.rb +++ b/lib/gitlab/sherlock/line_sample.rb @@ -3,15 +3,31 @@ module Gitlab class LineSample attr_reader :duration, :events + # duration - The execution time in milliseconds. + # events - The amount of events. def initialize(duration, events) @duration = duration @events = events end + # Returns the sample duration percentage relative to the given duration. + # + # Example: + # + # sample.duration # => 150 + # sample.percentage_of(1500) # => 10.0 + # + # total_duration - The total duration to compare with. + # + # Returns a float def percentage_of(total_duration) (duration.to_f / total_duration) * 100.0 end + # Returns true if the current sample takes up the majority of the given + # duration. + # + # total_duration - The total duration to compare with. def majority_of?(total_duration) percentage_of(total_duration) >= 30 end diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb index 8c0b77dce1..5ac265618a 100644 --- a/lib/gitlab/sherlock/location.rb +++ b/lib/gitlab/sherlock/location.rb @@ -5,15 +5,19 @@ module Gitlab SHERLOCK_DIR = File.dirname(__FILE__) + # Creates a new Location from a `Thread::Backtrace::Location`. def self.from_ruby_location(location) new(location.path, location.lineno) end + # path - The full path of the frame as a String. + # line - The line number of the frame as a Fixnum. def initialize(path, line) @path = path @line = line end + # Returns true if the current frame originated from the application. def application? @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR) end diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb index fca7be858e..687332fc5f 100644 --- a/lib/gitlab/sherlock/middleware.rb +++ b/lib/gitlab/sherlock/middleware.rb @@ -10,6 +10,7 @@ module Gitlab @app = app end + # env - A Hash containing Rack environment details. def call(env) if instrument?(env) call_with_instrumentation(env) @@ -19,7 +20,7 @@ module Gitlab end def call_with_instrumentation(env) - trans = Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI']) + trans = transaction_from_env(env) retval = trans.run { @app.call(env) } Sherlock.collection.add(trans) @@ -31,6 +32,10 @@ module Gitlab !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES && env['REQUEST_URI'] !~ IGNORE_PATHS) end + + def transaction_from_env(env) + Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI']) + end end end end diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb index af76e6fd2b..4917c4ae2a 100644 --- a/lib/gitlab/sherlock/query.rb +++ b/lib/gitlab/sherlock/query.rb @@ -3,6 +3,7 @@ module Gitlab class Query attr_reader :id, :query, :started_at, :finished_at, :backtrace + # SQL identifiers that should be prefixed with newlines. PREFIX_NEWLINE = / \s+(FROM |(LEFT|RIGHT)?INNER\s+JOIN @@ -12,11 +13,20 @@ module Gitlab |GROUP\s+BY |ORDER\s+BY |LIMIT - |OFFSET)\s+ - /ix + |OFFSET)\s+/ix # Vim indent breaks when this is on a newline :< + # Creates a new Query using a String and a separate Array of bindings. + # + # query - A String containing a SQL query, optionally with numeric + # placeholders (`$1`, `$2`, etc). + # + # bindings - An Array of ActiveRecord columns and their values. + # started_at - The start time of the query as a Time-like object. + # finished_at - The completion time of the query as a Time-like object. + # + # Returns a new Query object. def self.new_with_bindings(query, bindings, started_at, finished_at) - bindings.each_with_index do |(column, value), index| + bindings.each_with_index do |(_, value), index| quoted_value = ActiveRecord::Base.connection.quote(value) query = query.gsub("$#{index + 1}", quoted_value) @@ -25,6 +35,9 @@ module Gitlab new(query, started_at, finished_at) end + # query - The SQL query as a String (without placeholders). + # started_at - The start time of the query as a Time-like object. + # finished_at - The completion time of the query as a Time-like object. def initialize(query, started_at, finished_at) @id = SecureRandom.uuid @query = query @@ -39,6 +52,7 @@ module Gitlab end end + # Returns the query duration in milliseconds. def duration @duration ||= (@finished_at - @started_at) * 1000.0 end @@ -47,18 +61,22 @@ module Gitlab @id end + # Returns a human readable version of the query. def formatted_query @formatted_query ||= format_sql(@query) end + # Returns the last application frame of the backtrace. def last_application_frame @last_application_frame ||= @backtrace.find(&:application?) end + # Returns an Array of application frames (excluding Gems and the likes). def application_backtrace @application_backtrace ||= @backtrace.select(&:application?) end + # Returns the query plan as a String. def explain unless @explain ActiveRecord::Base.connection.transaction do diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb index 5cb3e86aa4..4641f15ee3 100644 --- a/lib/gitlab/sherlock/transaction.rb +++ b/lib/gitlab/sherlock/transaction.rb @@ -4,11 +4,12 @@ module Gitlab attr_reader :id, :type, :path, :queries, :file_samples, :started_at, :finished_at + # type - The type of transaction (e.g. "GET", "POST", etc) + # path - The path of the transaction (e.g. the HTTP request path) def initialize(type, path) @id = SecureRandom.uuid @type = type @path = path - @duration = 0 @queries = [] @file_samples = [] @started_at = nil @@ -16,6 +17,7 @@ module Gitlab @thread = Thread.current end + # Runs the transaction and returns the block's return value. def run @started_at = Time.now @@ -30,34 +32,43 @@ module Gitlab retval end + # Returns the duration in seconds. def duration - @started_at && @finished_at ? @finished_at - @started_at : 0 + @duration ||= started_at && finished_at ? finished_at - started_at : 0 end def to_param @id end + # Returns the queries sorted in descending order by their durations. def sorted_queries @queries.sort { |a, b| b.duration <=> a.duration } end + # Returns the file samples sorted in descending order by their durations. def sorted_file_samples @file_samples.sort { |a, b| b.duration <=> a.duration } end + # Finds a query by the given ID. + # + # id - The query ID as a String. + # + # Returns a Query object if one could be found, nil otherwise. def find_query(id) @queries.find { |query| query.id == id } end + # Finds a file sample by the given ID. + # + # id - The query ID as a String. + # + # Returns a FileSample object if one could be found, nil otherwise. def find_file_sample(id) @file_samples.find { |sample| sample.id == id } end - def track_query(query, bindings, start, finish) - @queries << Query.new_with_bindings(query, bindings, start, finish) - end - def profile_lines retval = nil @@ -70,6 +81,12 @@ module Gitlab retval end + private + + def track_query(query, bindings, start, finish) + @queries << Query.new_with_bindings(query, bindings, start, finish) + end + def subscribe_to_active_record ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data| # In case somebody uses a multi-threaded server locally (e.g. Puma) we diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb new file mode 100644 index 0000000000..a8a9d6fc7b --- /dev/null +++ b/spec/lib/gitlab/sherlock/collection_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::Collection do + let(:collection) { described_class.new } + + let(:transaction) do + Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures') + end + + describe '#add' do + it 'adds a new transaction' do + collection.add(transaction) + + expect(collection).to_not be_empty + end + + it 'is aliased as <<' do + collection << transaction + + expect(collection).to_not be_empty + end + end + + describe '#each' do + it 'iterates over every transaction' do + collection.add(transaction) + + expect { |b| collection.each(&b) }.to yield_with_args(transaction) + end + end + + describe '#clear' do + it 'removes all transactions' do + collection.add(transaction) + + collection.clear + + expect(collection).to be_empty + end + end + + describe '#empty?' do + it 'returns true for an empty collection' do + expect(collection).to be_empty + end + + it 'returns false for a collection with a transaction' do + collection.add(transaction) + + expect(collection).to_not be_empty + end + end + + describe '#find_transaction' do + it 'returns the transaction for the given ID' do + collection.add(transaction) + + expect(collection.find_transaction(transaction.id)).to eq(transaction) + end + + it 'returns nil when no transaction could be found' do + collection.add(transaction) + + expect(collection.find_transaction('cats')).to be_nil + end + end + + describe '#newest_first' do + it 'returns transactions sorted from new to old' do + trans1 = Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures') + trans2 = Gitlab::Sherlock::Transaction.new('POST', '/more_cat_pictures') + + allow(trans1).to receive(:finished_at).and_return(Time.utc(2015, 1, 1)) + allow(trans2).to receive(:finished_at).and_return(Time.utc(2015, 1, 2)) + + collection.add(trans1) + collection.add(trans2) + + expect(collection.newest_first).to eq([trans2, trans1]) + end + end +end diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb new file mode 100644 index 0000000000..f05a59f56f --- /dev/null +++ b/spec/lib/gitlab/sherlock/file_sample_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::FileSample do + let(:sample) { described_class.new(__FILE__, [], 150.4, 2) } + + describe '#id' do + it 'returns the ID' do + expect(sample.id).to be_an_instance_of(String) + end + end + + describe '#file' do + it 'returns the file path' do + expect(sample.file).to eq(__FILE__) + end + end + + describe '#line_samples' do + it 'returns the line samples' do + expect(sample.line_samples).to eq([]) + end + end + + describe '#events' do + it 'returns the total number of events' do + expect(sample.events).to eq(2) + end + end + + describe '#duration' do + it 'returns the total execution time' do + expect(sample.duration).to eq(150.4) + end + end + + describe '#relative_path' do + it 'returns the relative path' do + expect(sample.relative_path). + to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb') + end + end + + describe '#to_param' do + it 'returns the sample ID' do + expect(sample.to_param).to eq(sample.id) + end + end + + describe '#source' do + it 'returns the contents of the file' do + expect(sample.source).to eq(File.read(__FILE__)) + end + end +end diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb new file mode 100644 index 0000000000..8f2e129971 --- /dev/null +++ b/spec/lib/gitlab/sherlock/line_profiler_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::LineProfiler do + let(:profiler) { described_class.new } + + describe '#profile' do + it 'runs the profiler when using MRI' do + allow(profiler).to receive(:mri?).and_return(true) + allow(profiler).to receive(:profile_mri) + + profiler.profile { 'cats' } + end + + it 'raises NotImplementedError when profiling an unsupported platform' do + allow(profiler).to receive(:mri?).and_return(false) + + expect { profiler.profile { 'cats' } }.to raise_error(NotImplementedError) + end + end + + describe '#profile_mri' do + it 'returns an Array containing the return value and profiling samples' do + allow(profiler).to receive(:lineprof). + and_yield. + and_return({ __FILE__ => [[0, 0, 0, 0]] }) + + retval, samples = profiler.profile_mri { 42 } + + expect(retval).to eq(42) + expect(samples).to eq([]) + end + end + + describe '#aggregate_rblineprof' do + let(:raw_samples) do + { __FILE__ => [[30000, 30000, 5, 0], [15000, 15000, 4, 0]] } + end + + it 'returns an Array of FileSample objects' do + samples = profiler.aggregate_rblineprof(raw_samples) + + expect(samples).to be_an_instance_of(Array) + expect(samples[0]).to be_an_instance_of(Gitlab::Sherlock::FileSample) + end + + describe 'the first FileSample object' do + let(:file_sample) do + profiler.aggregate_rblineprof(raw_samples)[0] + end + + it 'uses the correct file path' do + expect(file_sample.file).to eq(__FILE__) + end + + it 'contains a list of line samples' do + line_sample = file_sample.line_samples[0] + + expect(line_sample).to be_an_instance_of(Gitlab::Sherlock::LineSample) + + expect(line_sample.duration).to eq(15.0) + expect(line_sample.events).to eq(4) + end + + it 'contains the total file execution time' do + expect(file_sample.duration).to eq(30.0) + end + + it 'contains the total amount of file events' do + expect(file_sample.events).to eq(5) + end + end + end +end diff --git a/spec/lib/gitlab/sherlock/line_sample_spec.rb b/spec/lib/gitlab/sherlock/line_sample_spec.rb new file mode 100644 index 0000000000..5f02f6a321 --- /dev/null +++ b/spec/lib/gitlab/sherlock/line_sample_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::LineSample do + let(:sample) { described_class.new(150.0, 4) } + + describe '#duration' do + it 'returns the duration' do + expect(sample.duration).to eq(150.0) + end + end + + describe '#events' do + it 'returns the amount of events' do + expect(sample.events).to eq(4) + end + end + + describe '#percentage_of' do + it 'returns the percentage of 1500.0' do + expect(sample.percentage_of(1500.0)).to be_within(0.1).of(10.0) + end + end + + describe '#majority_of' do + it 'returns true if the sample takes up the majority of the given duration' do + expect(sample.majority_of?(500.0)).to eq(true) + end + + it "returns false if the sample doesn't take up the majority of the given duration" do + expect(sample.majority_of?(1500.0)).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/sherlock/location_spec.rb b/spec/lib/gitlab/sherlock/location_spec.rb new file mode 100644 index 0000000000..b295a624b3 --- /dev/null +++ b/spec/lib/gitlab/sherlock/location_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::Location do + let(:location) { described_class.new(__FILE__, 1) } + + describe 'from_ruby_location' do + it 'creates a Location from a Thread::Backtrace::Location' do + input = caller_locations[0] + output = described_class.from_ruby_location(input) + + expect(output).to be_an_instance_of(described_class) + expect(output.path).to eq(input.path) + expect(output.line).to eq(input.lineno) + end + end + + describe '#path' do + it 'returns the file path' do + expect(location.path).to eq(__FILE__) + end + end + + describe '#line' do + it 'returns the line number' do + expect(location.line).to eq(1) + end + end + + describe '#application?' do + it 'returns true for an application frame' do + expect(location.application?).to eq(true) + end + + it 'returns false for a non application frame' do + loc = described_class.new('/tmp/cats.rb', 1) + + expect(loc.application?).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb new file mode 100644 index 0000000000..aa74fc53a7 --- /dev/null +++ b/spec/lib/gitlab/sherlock/middleware_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::Middleware do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + describe '#call' do + describe 'when instrumentation is enabled' do + it 'instruments a request' do + allow(middleware).to receive(:instrument?).and_return(true) + allow(middleware).to receive(:call_with_instrumentation) + + middleware.call({}) + end + end + + describe 'when instrumentation is disabled' do + it "doesn't instrument a request" do + allow(middleware).to receive(:instrument).and_return(false) + allow(app).to receive(:call) + + middleware.call({}) + end + end + end + + describe '#call_with_instrumentation' do + it 'instruments a request' do + trans = double(:transaction) + retval = 'cats are amazing' + env = {} + + allow(app).to receive(:call).with(env).and_return(retval) + allow(middleware).to receive(:transaction_from_env).and_return(trans) + allow(trans).to receive(:run).and_yield.and_return(retval) + allow(Gitlab::Sherlock.collection).to receive(:add).with(trans) + + middleware.call_with_instrumentation(env) + end + end + + describe '#instrument?' do + it 'returns false for a text/css request' do + env = { 'HTTP_ACCEPT' => 'text/css', 'REQUEST_URI' => '/' } + + expect(middleware.instrument?(env)).to eq(false) + end + + it 'returns false for a request to a Sherlock route' do + env = { + 'HTTP_ACCEPT' => 'text/html', + 'REQUEST_URI' => '/sherlock/transactions' + } + + expect(middleware.instrument?(env)).to eq(false) + end + + it 'returns true for a request that should be instrumented' do + env = { + 'HTTP_ACCEPT' => 'text/html', + 'REQUEST_URI' => '/cats' + } + + expect(middleware.instrument?(env)).to eq(true) + end + end + + describe '#transaction_from_env' do + it 'returns a Transaction' do + env = { + 'HTTP_ACCEPT' => 'text/html', + 'REQUEST_URI' => '/cats' + } + + expect(middleware.transaction_from_env(env)). + to be_an_instance_of(Gitlab::Sherlock::Transaction) + end + end +end diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb new file mode 100644 index 0000000000..b15a125a40 --- /dev/null +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::Query do + let(:started_at) { Time.utc(2015, 1, 1) } + let(:finished_at) { started_at + 5 } + + let(:query) do + described_class.new('SELECT COUNT(*) FROM users', started_at, finished_at) + end + + describe 'new_with_bindings' do + it 'returns a Query' do + sql = 'SELECT COUNT(*) FROM users WHERE id = $1' + bindings = [[double(:column), 10]] + + query = described_class. + new_with_bindings(sql, bindings, started_at, finished_at) + + expect(query.query).to eq('SELECT COUNT(*) FROM users WHERE id = 10;') + end + end + + describe '#id' do + it 'returns a String' do + expect(query.id).to be_an_instance_of(String) + end + end + + describe '#query' do + it 'returns the query with a trailing semi-colon' do + expect(query.query).to eq('SELECT COUNT(*) FROM users;') + end + end + + describe '#started_at' do + it 'returns the start time' do + expect(query.started_at).to eq(started_at) + end + end + + describe '#finished_at' do + it 'returns the completion time' do + expect(query.finished_at).to eq(finished_at) + end + end + + describe '#backtrace' do + it 'returns the backtrace' do + expect(query.backtrace).to be_an_instance_of(Array) + end + end + + describe '#duration' do + it 'returns the duration in milliseconds' do + expect(query.duration).to be_within(0.1).of(5000.0) + end + end + + describe '#to_param' do + it 'returns the query ID' do + expect(query.to_param).to eq(query.id) + end + end + + describe '#formatted_query' do + it 'returns a formatted version of the query' do + expect(query.formatted_query).to eq(<<-EOF.strip) +SELECT COUNT(*) +FROM users; + EOF + end + end + + describe '#last_application_frame' do + it 'returns the last application frame' do + frame = query.last_application_frame + + expect(frame).to be_an_instance_of(Gitlab::Sherlock::Location) + expect(frame.path).to eq(__FILE__) + end + end + + describe '#application_backtrace' do + it 'returns an Array of application frames' do + frames = query.application_backtrace + + expect(frames).to be_an_instance_of(Array) + expect(frames).to_not be_empty + + frames.each do |frame| + expect(frame.path).to start_with(Rails.root.to_s) + end + end + end + + describe '#explain' do + it 'returns the query plan as a String' do + lines = [ + ['Aggregate (cost=123 rows=1)'], + [' -> Index Only Scan using index_cats_are_amazing'] + ] + + result = double(:result, :values => lines) + + allow(query).to receive(:raw_explain).and_return(result) + + expect(query.explain).to eq(<<-EOF.strip) +Aggregate (cost=123 rows=1) + -> Index Only Scan using index_cats_are_amazing + EOF + end + end +end diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb new file mode 100644 index 0000000000..bb4ff42e6e --- /dev/null +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +describe Gitlab::Sherlock::Transaction do + let(:transaction) { described_class.new('POST', '/cat_pictures') } + + describe '#id' do + it 'returns the transaction ID' do + expect(transaction.id).to be_an_instance_of(String) + end + end + + describe '#type' do + it 'returns the type' do + expect(transaction.type).to eq('POST') + end + end + + describe '#path' do + it 'returns the path' do + expect(transaction.path).to eq('/cat_pictures') + end + end + + describe '#queries' do + it 'returns an Array of queries' do + expect(transaction.queries).to be_an_instance_of(Array) + end + end + + describe '#file_samples' do + it 'returns an Array of file samples' do + expect(transaction.file_samples).to be_an_instance_of(Array) + end + end + + describe '#started_at' do + it 'returns the start time' do + allow(transaction).to receive(:profile_lines).and_yield + + transaction.run { 'cats are amazing' } + + expect(transaction.started_at).to be_an_instance_of(Time) + end + end + + describe '#finished_at' do + it 'returns the completion time' do + allow(transaction).to receive(:profile_lines).and_yield + + transaction.run { 'cats are amazing' } + + expect(transaction.finished_at).to be_an_instance_of(Time) + end + end + + describe '#run' do + it 'runs the transaction' do + allow(transaction).to receive(:profile_lines).and_yield + + retval = transaction.run { 'cats are amazing' } + + expect(retval).to eq('cats are amazing') + end + end + + describe '#duration' do + it 'returns the duration in seconds' do + start_time = Time.now + + allow(transaction).to receive(:started_at).and_return(start_time) + allow(transaction).to receive(:finished_at).and_return(start_time + 5) + + expect(transaction.duration).to be_within(0.1).of(5.0) + end + end + + describe '#to_param' do + it 'returns the transaction ID' do + expect(transaction.to_param).to eq(transaction.id) + end + end + + describe '#sorted_queries' do + it 'returns the queries in descending order' do + start_time = Time.now + + query1 = Gitlab::Sherlock::Query.new('SELECT 1', start_time, start_time) + + query2 = Gitlab::Sherlock::Query. + new('SELECT 2', start_time, start_time + 5) + + transaction.queries << query1 + transaction.queries << query2 + + expect(transaction.sorted_queries).to eq([query2, query1]) + end + end + + describe '#sorted_file_samples' do + it 'returns the file samples in descending order' do + sample1 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1) + sample2 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 15.0, 1) + + transaction.file_samples << sample1 + transaction.file_samples << sample2 + + expect(transaction.sorted_file_samples).to eq([sample2, sample1]) + end + end + + describe '#find_query' do + it 'returns a Query when found' do + query = Gitlab::Sherlock::Query.new('SELECT 1', Time.now, Time.now) + + transaction.queries << query + + expect(transaction.find_query(query.id)).to eq(query) + end + + it 'returns nil when no query could be found' do + expect(transaction.find_query('cats')).to be_nil + end + end + + describe '#find_file_sample' do + it 'returns a FileSample when found' do + sample = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1) + + transaction.file_samples << sample + + expect(transaction.find_file_sample(sample.id)).to eq(sample) + end + + it 'returns nil when no file sample could be found' do + expect(transaction.find_file_sample('cats')).to be_nil + end + end + + describe '#profile_lines' do + describe 'when line profiling is enabled' do + it 'yields the block using the line profiler' do + allow(Gitlab::Sherlock).to receive(:enable_line_profiler?). + and_return(true) + + allow_any_instance_of(Gitlab::Sherlock::LineProfiler). + to receive(:profile).and_return('cats are amazing', []) + + retval = transaction.profile_lines { 'cats are amazing' } + + expect(retval).to eq('cats are amazing') + end + end + + describe 'when line profiling is disabled' do + it 'yields the block' do + allow(Gitlab::Sherlock).to receive(:enable_line_profiler?). + and_return(false) + + retval = transaction.profile_lines { 'cats are amazing' } + + expect(retval).to eq('cats are amazing') + end + end + end +end From 126a2428cd7fb5b2766da10373f72a370aa68ba5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 5 Nov 2015 18:06:05 +0100 Subject: [PATCH 416/443] Updated profiling guides for Sherlock --- doc/development/profiling.md | 45 +++++++----------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/doc/development/profiling.md b/doc/development/profiling.md index 80c86ef921..e244ad4e88 100644 --- a/doc/development/profiling.md +++ b/doc/development/profiling.md @@ -4,11 +4,15 @@ To make it easier to track down performance problems GitLab comes with a set of profiling tools, some of these are available by default while others need to be explicitly enabled. -## rack-mini-profiler +## Sherlock -This Gem is enabled by default in development only. It allows you to see the -timings of the various components that made up a web request (e.g. the SQL -queries executed and their execution timings). +Sherlock is a custom profiling tool built into GitLab. Sherlock is _only_ +available when running GitLab in development mode _and_ when setting the +environment variable `ENABLE_SHERLOCK` to a non empty value. For example: + + ENABLE_SHERLOCK=1 bundle exec rails s + +Recorded transactions can be found by navigating to `/sherlock/transactions`. ## Bullet @@ -21,36 +25,3 @@ starting GitLab. For example: Bullet will log query problems to both the Rails log as well as the Chrome console. - -## ActiveRecord Query Trace - -This Gem adds backtraces for every ActiveRecord query in the Rails console. This -can be useful to track down where a query was executed. Because this Gem adds -quite a bit of noise (5-10 extra lines per ActiveRecord query) it's disabled by -default. To use this Gem you'll need to set `ENABLE_QUERY_TRACE` to a non empty -file before starting GitLab. For example: - - ENABLE_QUERY_TRACE=true bundle exec rails s - -## rack-lineprof - -This is a Gem that can trace the execution time of code on a per line basis. -Because this Gem can add quite a bit of overhead it's disabled by default. To -enable it, set the environment variable `ENABLE_LINEPROF` to a non-empty value. -For example: - - ENABLE_LINEPROF=true bundle exec rails s - -Once enabled you'll need to add a query string parameter to a request to -actually profile code execution. The name of the parameter is `lineprof` and -should be set to a regular expression (minus the starting/ending slash) used to -select what files to profile. To profile all files containing "foo" somewhere in -the path you'd use the following parameter: - - ?lineprof=foo - -Or when filtering for files containing "foo" and "bar" in their path: - - ?lineprof=foo|bar - -Once set the profiling output will be displayed in your terminal. From db46d49c0ed7e34a9679b0475480306d92b0de31 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 11:21:50 +0100 Subject: [PATCH 417/443] Fixed Hash key style in Sherlock::Query spec --- spec/lib/gitlab/sherlock/query_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb index b15a125a40..a9afef5dc1 100644 --- a/spec/lib/gitlab/sherlock/query_spec.rb +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -100,7 +100,7 @@ FROM users; [' -> Index Only Scan using index_cats_are_amazing'] ] - result = double(:result, :values => lines) + result = double(:result, values: lines) allow(query).to receive(:raw_explain).and_return(result) From cdaa97443e89a08d857a244e6e8ab0235db9746d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 11:30:57 +0100 Subject: [PATCH 418/443] Added navigation link to Sherlock --- app/views/layouts/header/_default.html.haml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index c31b1cbe9a..c08a7b8074 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -21,6 +21,11 @@ %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('plus fw') + - if Gitlab::Sherlock.enabled? + %li + = link_to sherlock_transactions_path, title: 'Sherlock Transactions', + data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('tachometer fw') %li = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('sign-out') From 7b5fd8742e6112491f61f27dcca2d8e441cc33a1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 12:36:01 +0100 Subject: [PATCH 419/443] Track the amount of times views are rendered --- .../transactions/_file_samples.html.haml | 2 + config/locales/sherlock.en.yml | 1 + lib/gitlab/sherlock/transaction.rb | 57 ++++++++++++++----- spec/lib/gitlab/sherlock/transaction_spec.rb | 57 +++++++++++++++++++ 4 files changed, 103 insertions(+), 14 deletions(-) diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml index 0afdbc8dff..4349c9b7ac 100644 --- a/app/views/sherlock/transactions/_file_samples.html.haml +++ b/app/views/sherlock/transactions/_file_samples.html.haml @@ -7,6 +7,7 @@ %thead %tr %th= t('sherlock.time_inclusive') + %th= t('sherlock.count') %th= t('sherlock.path') %th %tbody @@ -15,6 +16,7 @@ %td = sample.duration.round(2) = t('sherlock.milliseconds') + %td= @transaction.view_counts.fetch(sample.file, 1) %td= sample.relative_path %td = link_to(t('sherlock.view'), diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml index 5c146b172b..683b09dc32 100644 --- a/config/locales/sherlock.en.yml +++ b/config/locales/sherlock.en.yml @@ -34,3 +34,4 @@ en: query_plan: Query Plan events: Events percent: '%' + count: Count diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb index 4641f15ee3..d87a4c9bb4 100644 --- a/lib/gitlab/sherlock/transaction.rb +++ b/lib/gitlab/sherlock/transaction.rb @@ -2,7 +2,7 @@ module Gitlab module Sherlock class Transaction attr_reader :id, :type, :path, :queries, :file_samples, :started_at, - :finished_at + :finished_at, :view_counts # type - The type of transaction (e.g. "GET", "POST", etc) # path - The path of the transaction (e.g. the HTTP request path) @@ -15,20 +15,19 @@ module Gitlab @started_at = nil @finished_at = nil @thread = Thread.current + @view_counts = Hash.new(0) end # Runs the transaction and returns the block's return value. def run @started_at = Time.now - subscriber = subscribe_to_active_record - - retval = profile_lines { yield } + retval = with_subscriptions do + profile_lines { yield } + end @finished_at = Time.now - ActiveSupport::Notifications.unsubscribe(subscriber) - retval end @@ -81,21 +80,51 @@ module Gitlab retval end + def subscribe_to_active_record + ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data| + next unless same_thread? + + track_query(data[:sql].strip, data[:binds], start, finish) + end + end + + def subscribe_to_action_view + regex = /render_(template|partial)\.action_view/ + + ActiveSupport::Notifications.subscribe(regex) do |_, start, finish, _, data| + next unless same_thread? + + track_view(data[:identifier]) + end + end + private def track_query(query, bindings, start, finish) @queries << Query.new_with_bindings(query, bindings, start, finish) end - def subscribe_to_active_record - ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data| - # In case somebody uses a multi-threaded server locally (e.g. Puma) we - # _only_ want to track queries that originate from the transaction - # thread. - next unless Thread.current == @thread + def track_view(path) + @view_counts[path] += 1 + end - track_query(data[:sql].strip, data[:binds], start, finish) - end + def with_subscriptions + ar_subscriber = subscribe_to_active_record + av_subscriber = subscribe_to_action_view + + retval = yield + + ActiveSupport::Notifications.unsubscribe(ar_subscriber) + ActiveSupport::Notifications.unsubscribe(av_subscriber) + + retval + end + + # In case somebody uses a multi-threaded server locally (e.g. Puma) we + # _only_ want to track notifications that originate from the transaction + # thread. + def same_thread? + Thread.current == @thread end end end diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index bb4ff42e6e..bb49fb65cf 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -53,6 +53,16 @@ describe Gitlab::Sherlock::Transaction do end end + describe '#view_counts' do + it 'returns a Hash' do + expect(transaction.view_counts).to be_an_instance_of(Hash) + end + + it 'sets the default value of a key to 0' do + expect(transaction.view_counts['cats.rb']).to be_zero + end + end + describe '#run' do it 'runs the transaction' do allow(transaction).to receive(:profile_lines).and_yield @@ -162,4 +172,51 @@ describe Gitlab::Sherlock::Transaction do end end end + + describe '#subscribe_to_active_record' do + let(:subscription) { transaction.subscribe_to_active_record } + let(:time) { Time.now } + let(:query_data) { { sql: 'SELECT 1', binds: [] } } + + after do + ActiveSupport::Notifications.unsubscribe(subscription) + end + + it 'tracks executed queries' do + expect(transaction).to receive(:track_query). + with('SELECT 1', [], time, time) + + subscription.publish('test', time, time, nil, query_data) + end + + it 'only tracks queries triggered from the transaction thread' do + expect(transaction).to_not receive(:track_query) + + Thread.new { subscription.publish('test', time, time, nil, query_data) }. + join + end + end + + describe '#subscribe_to_action_view' do + let(:subscription) { transaction.subscribe_to_action_view } + let(:time) { Time.now } + let(:view_data) { { identifier: 'foo.rb' } } + + after do + ActiveSupport::Notifications.unsubscribe(subscription) + end + + it 'tracks rendered views' do + expect(transaction).to receive(:track_view).with('foo.rb') + + subscription.publish('test', time, time, nil, view_data) + end + + it 'only tracks views rendered from the transaction thread' do + expect(transaction).to_not receive(:track_view) + + Thread.new { subscription.publish('test', time, time, nil, view_data) }. + join + end + end end From 7f9f07023bbe4393620998d1be46cc65d836d5c8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 12:42:28 +0100 Subject: [PATCH 420/443] Truncate transaction paths to 70 characters This ensures that long URLs don't completely mess up the layout of the table. --- app/views/sherlock/transactions/index.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml index fb31131ba8..010e1a2a90 100644 --- a/app/views/sherlock/transactions/index.html.haml +++ b/app/views/sherlock/transactions/index.html.haml @@ -27,7 +27,9 @@ - @transactions.each do |trans| %tr %td= trans.type - %td= trans.path + %td + %span{title: trans.path} + = truncate(trans.path, length: 70) %td = trans.duration.round(2) = t('sherlock.seconds') From 68843a53e6cfe5493ad46dac58b469dd969a0edf Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 14:29:50 +0100 Subject: [PATCH 421/443] Added changelog entry for Sherlock --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 19f288d0c7..217dc4e004 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses From e357e4fbab79b0d834590b623644476a2ef980f5 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Mon, 9 Nov 2015 08:34:25 -0500 Subject: [PATCH 422/443] remove state_machine_patch.rb --- config/initializers/state_machine_patch.rb | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 config/initializers/state_machine_patch.rb diff --git a/config/initializers/state_machine_patch.rb b/config/initializers/state_machine_patch.rb deleted file mode 100644 index 51f0579436..0000000000 --- a/config/initializers/state_machine_patch.rb +++ /dev/null @@ -1,9 +0,0 @@ -# This is a patch to address the issue in https://github.com/pluginaweek/state_machine/issues/251 -# where gem 'state_machine' was not working for Rails 4.1 -module StateMachines - module Integrations - module ActiveModel - public :around_validation - end - end -end From ddd3dd72fec5d31fc76023e10f824cc079f65b63 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Mon, 9 Nov 2015 09:11:42 -0500 Subject: [PATCH 423/443] update Gemfile.lock --- Gemfile.lock | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dce728baf1..21cbf854c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -692,7 +692,13 @@ GEM activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) stamp (0.6.0) - state_machine (1.2.0) + state_machines (0.4.0) + state_machines-activemodel (0.3.0) + activemodel (~> 4.1) + state_machines (>= 0.4.0) + state_machines-activerecord (0.3.0) + activerecord (~> 4.1) + state_machines-activemodel (>= 0.3.0) stringex (2.5.2) systemu (2.6.5) task_list (1.0.2) @@ -913,7 +919,7 @@ DEPENDENCIES spring-commands-teaspoon (~> 0.0.2) sprockets (~> 2.12.3) stamp (~> 0.6.0) - state_machine (~> 1.2.0) + state_machines-activerecord (~> 0.3.0) task_list (~> 1.0.2) teaspoon (~> 1.0.0) teaspoon-jasmine (~> 2.2.0) From b67fdfff3c245538ee5a5e9360a2613b76ebada5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Nov 2015 15:30:50 +0100 Subject: [PATCH 424/443] Refactor release code a bit Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/releases_controller.rb | 8 +++++--- app/controllers/projects/tags_controller.rb | 8 -------- app/models/release.rb | 2 +- app/services/create_tag_service.rb | 8 ++++++-- app/services/delete_tag_service.rb | 4 +++- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index f69a4bc729..0825a4311c 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -10,9 +10,7 @@ class Projects::ReleasesController < Projects::ApplicationController end def update - description = params[:release][:description] - release.update_attributes(description: description) - release.save + release.update_attributes(release_params) redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) end @@ -26,4 +24,8 @@ class Projects::ReleasesController < Projects::ApplicationController def release @release ||= @project.releases.find_or_initialize_by(tag: @tag.name) end + + def release_params + params.require(:release).permit(:description) + end end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 670f5d3067..f512f01dc7 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -24,12 +24,6 @@ class Projects::TagsController < Projects::ApplicationController if result[:status] == :success @tag = result[:tag] - if params[:release_description] - release = @project.releases.find_or_initialize_by(tag: @tag.name) - release.update_attributes(description: params[:release_description]) - release.save - end - redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) else @error = result[:message] @@ -39,8 +33,6 @@ class Projects::TagsController < Projects::ApplicationController def destroy DeleteTagService.new(project, current_user).execute(params[:id]) - release = project.releases.find_by(tag: params[:id]) - release.destroy if release redirect_to namespace_project_tags_path(@project.namespace, @project) end diff --git a/app/models/release.rb b/app/models/release.rb index 05647839e8..e196b84eb1 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,5 +1,5 @@ class Release < ActiveRecord::Base belongs_to :project - validates :description, :project, presence: true + validates :description, :project, :tag, presence: true end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 1a7318048b..9917119fce 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -1,7 +1,7 @@ require_relative 'base_service' class CreateTagService < BaseService - def execute(tag_name, ref, message) + def execute(tag_name, ref, message, release_description = nil) valid_tag = Gitlab::GitRefValidator.validate(tag_name) if valid_tag == false return error('Tag name invalid') @@ -19,8 +19,12 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - push_data = create_push_data(project, current_user, new_tag) + if release_description + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: release_description) + end + push_data = create_push_data(project, current_user, new_tag) EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 0c83640113..de3352a675 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -11,8 +11,10 @@ class DeleteTagService < BaseService end if repository.rm_tag(tag_name) + release = project.releases.find_by(tag: tag_name) + release.destroy if release + push_data = build_push_data(tag) - EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) From b7619dad52504f8fc61bfb3b42e7f8bcc42dc06d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Nov 2015 15:39:18 +0100 Subject: [PATCH 425/443] Add missing param and title for tag Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/tags_controller.rb | 2 +- app/views/projects/releases/edit.html.haml | 1 + app/views/projects/tags/show.html.haml | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index f512f01dc7..cb39c2b878 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -19,7 +19,7 @@ class Projects::TagsController < Projects::ApplicationController def create result = CreateTagService.new(@project, current_user). - execute(params[:tag_name], params[:ref], params[:message]) + execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) if result[:status] == :success @tag = result[:tag] diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 7874141634..e7db09cdaa 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @tag.name, "Tags" = render "projects/commits/header_title" = render "projects/commits/head" diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index f95ae9edc4..ebe3718afc 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -4,8 +4,9 @@ .gray-content-block .pull-right - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn', title: 'Edit release notes' do - = icon("pencil") + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn', title: 'Edit release notes' do + = icon("pencil") = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse source code' do = icon('files-o') = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse commits' do @@ -35,4 +36,4 @@ = preserve do = markdown @release.description - else - This tag has no release notes yet. Press edit button to add one + This tag has no release notes. From 73cf0f1647806a4ce064707c6f1f416181de48ef Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 Nov 2015 15:54:13 +0100 Subject: [PATCH 426/443] Only load rblineprof when actually needed This ensures the application can still boot when the "development" group is not available. --- lib/gitlab/sherlock.rb | 1 - lib/gitlab/sherlock/line_profiler.rb | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/sherlock.rb b/lib/gitlab/sherlock.rb index c4b35b24ce..6360527a7a 100644 --- a/lib/gitlab/sherlock.rb +++ b/lib/gitlab/sherlock.rb @@ -1,5 +1,4 @@ require 'securerandom' -require 'rblineprof' if RUBY_ENGINE == 'ruby' module Gitlab module Sherlock diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb index 152749dcc3..aa1468bff6 100644 --- a/lib/gitlab/sherlock/line_profiler.rb +++ b/lib/gitlab/sherlock/line_profiler.rb @@ -40,6 +40,8 @@ module Gitlab # Profiles the given block using rblineprof (MRI only). def profile_mri + require 'rblineprof' + retval = nil samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield } From 4017a4fcbcb0ed1beb56718cf586c38bbb951527 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 9 Nov 2015 04:40:29 -0800 Subject: [PATCH 427/443] Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3. Switch back to github-linguist --- CHANGELOG | 1 + Gemfile | 10 +++------- Gemfile.lock | 25 +++++++++++++------------ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 217dc4e004..afb3d1086b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) + - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses diff --git a/Gemfile b/Gemfile index b0a7c9b945..51848621b6 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.19' +gem "gitlab_git", '~> 7.2.20' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -51,11 +51,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" gem 'gollum-lib', '~> 4.0.2' # Language detection -# GitLab fork of linguist does not require pygments/python dependency. -# New version of original gem also dropped pygments support but it has strict -# dependency to unstable rugged version. We have internal issue for replacing -# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052. -gem "gitlab-linguist", "~> 3.0.1", require: "linguist" +gem "github-linguist", "~> 4.7.0", require: "linguist" # API gem 'grape', '~> 0.6.1' @@ -185,7 +181,7 @@ gem 'ace-rails-ap', '~> 2.0.1' gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding -gem 'charlock_holmes', '~> 0.6.9.4' +gem 'charlock_holmes', '~> 0.7.3' gem "sass-rails", '~> 4.0.5' gem "coffee-rails", '~> 4.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index c602d40671..d358e23630 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,7 +175,7 @@ GEM activesupport (>= 3.2) equalizer (0.0.11) erubis (2.7.0) - escape_utils (0.2.4) + escape_utils (1.1.0) eventmachine (1.0.8) excon (0.45.4) execjs (2.6.0) @@ -266,6 +266,11 @@ GEM json get_process_mem (0.2.0) gherkin-ruby (0.3.2) + github-linguist (4.7.0) + charlock_holmes (~> 0.7.3) + escape_utils (~> 1.1.0) + mime-types (>= 1.19) + rugged (>= 0.23.0b) github-markup (1.3.3) gitlab-flowdock-git-hook (1.0.1) flowdock (~> 0.7) @@ -276,17 +281,13 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab-linguist (3.0.1) - charlock_holmes (~> 0.6.6) - escape_utils (~> 0.2.4) - mime-types (~> 1.19) gitlab_emoji (0.1.1) gemojione (~> 2.0) - gitlab_git (7.2.19) + gitlab_git (7.2.20) activesupport (~> 4.0) - charlock_holmes (~> 0.6) - gitlab-linguist (~> 3.0) - rugged (~> 0.22.2) + charlock_holmes (~> 0.7.3) + github-linguist (~> 4.7.0) + rugged (~> 0.23.3) gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) @@ -794,7 +795,7 @@ DEPENDENCIES capybara (~> 2.4.0) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.9.0) - charlock_holmes (~> 0.6.9.4) + charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) colored (~> 1.2) colorize (~> 0.5.8) @@ -819,11 +820,11 @@ DEPENDENCIES foreman fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) + github-linguist (~> 4.7.0) github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.19) + gitlab_git (~> 7.2.20) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) From f526d43432447fe8de84ffc247b65e987ddfde84 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 9 Nov 2015 13:26:29 -0500 Subject: [PATCH 428/443] Update CHANGELOG [ci skip] --- CHANGELOG | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 78ce4f3bfd..4c0e9aa0fb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,8 +2,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses - Improved performance of replacing references in comments - Show last project commit to default branch on project home page @@ -19,7 +17,6 @@ v 8.2.0 (unreleased) - Enable shared runners to all new projects - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - - Use issue editor as cross reference comment author when issue is edited with a new mention. - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Add "New file" link to dropdown on project page - Include commit logs in project search @@ -29,9 +26,14 @@ v 8.2.0 (unreleased) - New design for project graphs page - Fix incoming email config defaults +v 8.1.4 + - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) + - Prevent redirect loop when home_page_url is set to the root URL + v 8.1.3 + - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Spread out runner contacted_at updates - - New design for user profile page + - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication v 8.1.1 From 9822bce467b19e7b04e3e3c97bb1bcd9e4074161 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 9 Nov 2015 14:24:26 -0500 Subject: [PATCH 429/443] Update monthly release issue template [ci skip] --- doc/release/monthly.md | 86 +++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index bd8a67d1d8..4925816daa 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -25,68 +25,84 @@ If the release is falling behind immediately warn the team. ## Create an overall issue and follow it -Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching. -Replace the dates with actual dates based on the number of workdays before the release. -All steps from issue template are explained below +Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with +the `release` label for easier searching. Replace the dates with actual dates +based on the number of workdays before the release. All steps from issue +template are explained below: ``` -Xth: (7 working days before the 22nd) +### Xth: (7 working days before the 22nd) -- [ ] Triage the omnibus-gitlab milestone +- [ ] Triage the [Omnibus milestone] -Xth: (6 working days before the 22nd) +### Xth: (6 working days before the 22nd) -- [ ] Merge CE master in to EE master via merge request (#LINK) +- [ ] Merge CE `master` into EE `master` via merge request (#LINK) - [ ] Determine QA person and notify this person - [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary -- [ ] Create CE, EE, CI RC1 versions (#LINK) -- [ ] Build RC1 packages (EE first) (#LINK) +- [ ] Create CE and EE RC1 versions (#LINK) +- [ ] Build RC1 packages -Xth: (5 working days before the 22nd) +### Xth: (5 working days before the 22nd) - [ ] Do QA and fix anything coming out of it (#LINK) -- [ ] Close the omnibus-gitlab milestone -- [ ] Prepare the blog post (#LINK) +- [ ] Close the [Omnibus milestone] +- [ ] Prepare the [blog post] -Xth: (4 working days before the 22nd) +### Xth: (4 working days before the 22nd) -- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) -- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) -- [ ] Create regression issues (CE, CI) (#LINK) -- [ ] Tweet about rc1 (#LINK), proposed text: +- [ ] Update GitLab.com with RC1 +- [ ] Create the regression issue in the CE issue tracker: -> GitLab x.x.0.rc1 is available https://packages.gitlab.com/gitlab/unstable Use at your own risk. Please link regressions issues from LINK_TO_REGRESSION_ISSUE + > This is a meta issue to index possible regressions in this monthly release + > and any patch versions. + > + > Please do not raise or discuss issues directly in this issue but link to + > issues that might warrant a patch release. If there is a Merge Request + > that fixes the issue, please link to that as well. + > + > Please only post one regression issue and/or merge request per comment. + > Comments will be updated by the release manager as they are addressed. -Xth: (3 working days before the 22nd) +- [ ] Tweet about RC1 release: -- [ ] Merge CE stable branch into EE stable branch + > GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable + > Use at your own risk. Please link regressions issues from + > LINK_TO_REGRESSION_ISSUE -Xth: (2 working days before the 22nd) +### Xth: (3 working days before the 22nd) -- [ ] Check that everyone is mentioned on the blog post using `@all` (the reviewer should have done this one working day ago) -- [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com) +- [ ] Merge `x-y-stable` into `x-y-stable-ee` +- [ ] Check that everyone is mentioned on the [blog post] using `@all` -Xth: (1 working day before the 22nd) +### Xth: (2 working days before the 22nd) -- [ ] Merge CE stable into EE stable -- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow) +- [ ] Check that MVP is added to the [MVP page] + +### Xth: (1 working day before the 22nd) + +- [ ] Merge `x-y-stable` into `x-y-stable-ee` +- [ ] Create CE and EE release candidates - [ ] Create Omnibus tags and build packages for the latest release candidates -- [ ] Update GitLab.com with the latest RC (#LINK) -- [ ] Update ci.gitLab.com with the latest RC (#LINK) +- [ ] Update GitLab.com with the latest RC -22nd before 1200 CET: +### 22nd before 1200 CET: Release before 1200 CET / 2AM PST, to make sure the majority of our users get the new version on the 22nd and there is sufficient time in the European workday to quickly fix any issues. -- [ ] Merge CE stable into EE stable (#LINK) -- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK) +- [ ] Merge `x-y-stable` into `x-y-stable-ee` +- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) - [ ] Create the 'x.y.0' version on version.gitlab.com -- [ ] Try to do before 1100 CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) -- [ ] Try to do before 1200 CET: Publish the release blog post (#LINK) -- [ ] Tweet about the release (blog post) (#LINK) -- [ ] Schedule a second tweet of the release announcement with the same text at 1800 CET / 8AM PST +- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages) +- [ ] Try to do before 1200 CET: Publish the release [blog post] +- [ ] Tweet about the release +- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST + +[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE +[blog post]: LINK_TO_WIP_BLOG_POST +[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html ``` - - - From 746e49fee9a28f509f115074d9985830de45513d Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Tue, 3 Nov 2015 16:44:14 +0200 Subject: [PATCH 430/443] Display target branch on MR list when it is different from project's default --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 1 + .../merge_requests/_merge_request.html.haml | 5 +++++ features/project/merge_requests.feature | 9 +++++++++ features/steps/project/merge_requests.rb | 20 +++++++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 44c78ed62c..fa0b361459 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.2.0 (unreleased) - Allow groups to appear in the search results if the group owner allows it - New design for project graphs page - Fix incoming email config defaults + - MR target branch is now visible on a list view when it is different from project's default one v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 16c4238662..b0788a2d07 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -31,6 +31,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) + @merge_requests = @merge_requests.preload(:target_project) respond_to do |format| format.html diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 300a371529..c5234c0618 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -41,6 +41,11 @@ %span %i.fa.fa-clock-o = merge_request.milestone.title + - if merge_request.target_project.default_branch != merge_request.target_branch +   + %span + %i.fa.fa-code-fork + = merge_request.target_branch - if merge_request.tasks? %span.task-status = merge_request.task_status diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index f423c3ba54..6cd081c868 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -16,6 +16,15 @@ Feature: Project Merge Requests When I visit project "Shop" merge requests page Then I should see merge request "Bug NS-05" with CI status + Scenario: I should not see target branch name when it is project's default branch + Then I should see "Bug NS-04" in merge requests + And I should not see "master" branch + + Scenario: I should see target branch when it is different from default + Given project "Shop" have "Bug NS-06" open merge request + When I visit project "Shop" merge requests page + Then I should see "other_branch" branch + Scenario: I should see rejected merge requests Given I click link "Closed" Then I should see "Feature NS-03" in merge requests diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 92ec14d0d7..d5f2c4209a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -40,6 +40,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(page).to have_content "Bug NS-04" end + step 'I should not see "master" branch' do + expect(page).not_to have_content "master" + end + + step 'I should see "other_branch" branch' do + expect(page).to have_content "other_branch" + end + step 'I should see "Bug NS-04" in merge requests' do expect(page).to have_content "Bug NS-04" end @@ -93,6 +101,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ) end + step 'project "Shop" have "Bug NS-06" open merge request' do + create(:merge_request, + title: "Bug NS-06", + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'other_branch', + author: project.users.first, + description: "# Description header" + ) + end + step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do create(:merge_request_with_diffs, title: "Bug NS-05", From f3bf0fe7aca0672c406141ff88fffe623e507bdc Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Tue, 10 Nov 2015 11:25:28 +0100 Subject: [PATCH 431/443] Fix duplicate entry for 8.1.0 release Fix duplicate entry for 8.1.0 release and remove unreleased tag. --- CHANGELOG | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d11076f484..843589eacf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,8 +56,6 @@ v 8.1.0 - Redirect to a default path if HTTP_REFERER is not set (Stan Hu) - Adds ability to create directories using the web editor (Ben Ford) - Cleanup stuck CI builds - -v 8.1.0 (unreleased) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Show notifications button when user is member of group rather than project (Grzegorz Bizon) - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge. From 1b7a2fc5363d7e2334f5c48940e8eca9d88354a6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Nov 2015 11:49:38 +0100 Subject: [PATCH 432/443] Improve Continuous Integration graphs page * fix commit duration graph * make graphs responsive * fix wrong padding * add a bit of explanation to colors Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/graphs/ci.html.haml | 16 +++++-- .../projects/graphs/ci/_build_times.haml | 17 +++---- app/views/projects/graphs/ci/_builds.haml | 48 +++++++++++-------- app/views/projects/graphs/ci/_overall.haml | 16 +++---- app/views/projects/graphs/commits.html.haml | 2 +- app/views/projects/graphs/show.html.haml | 2 +- lib/ci/charts.rb | 3 +- 7 files changed, 60 insertions(+), 44 deletions(-) diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index b2dfe97938..6fa77cc10c 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,10 +1,16 @@ - page_title "Continuous Integration", "Graphs" = render "header_title" = render 'head' -.gray-content-block - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +.gray-content-block.append-bottom-default + .oneline + A collection of graphs for Continuous Integration + #charts.ci-charts + .row + .col-md-6 + = render 'projects/graphs/ci/overall' + .col-md-6 + = render 'projects/graphs/ci/build_times' + + %hr = render 'projects/graphs/ci/builds' - = render 'projects/graphs/ci/build_times' -= render 'projects/graphs/ci/overall' diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index c3c2f57241..c58223fd39 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -1,21 +1,22 @@ -%fieldset - %legend +%div + %p.light Commit duration in minutes for last 30 commits - %canvas#build_timesChart.padded{width: 800, height: 300} + %canvas#build_timesChart{height: 200} :javascript var data = { labels : #{@charts[:build_times].labels.to_json}, datasets : [ { - fillColor : "#4A3", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", + fillColor : "rgba(220,220,220,0.5)", + strokeColor : "rgba(220,220,220,1)", + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, data : #{@charts[:build_times].build_times.to_json} } ] } var ctx = $("#build_timesChart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true}); + new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 1b0039fb83..8424745540 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -1,20 +1,30 @@ -%fieldset - %legend - Builds chart for last week +%h4 Build charts +%p +   + %span.cgreen + = icon("circle") + success +   + %span.cgray + = icon("circle") + all + +.prepend-top-default + %p.light + Builds for last week (#{date_from_to(Date.today - 7.days, Date.today)}) + %canvas#weekChart{height: 200} - %canvas#weekChart.padded{width: 800, height: 200} - -%fieldset - %legend +.prepend-top-default + %p.light Builds chart for last month (#{date_from_to(Date.today - 30.days, Date.today)}) + %canvas#monthChart{height: 200} - %canvas#monthChart.padded{width: 800, height: 300} - -%fieldset - %legend Builds chart for last year - %canvas#yearChart.padded{width: 800, height: 400} +.prepend-top-default + %p.light + Builds chart for last year + %canvas#yearChart.padded{height: 250} - [:week, :month, :year].each do |scope| :javascript @@ -22,20 +32,20 @@ labels : #{@charts[scope].labels.to_json}, datasets : [ { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - pointColor : "rgba(220,220,220,1)", + fillColor : "#7f8fa4", + strokeColor : "#7f8fa4", + pointColor : "#7f8fa4", pointStrokeColor : "#EEE", data : #{@charts[scope].total.to_json} }, { - fillColor : "#4A3", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", + fillColor : "#44aa22", + strokeColor : "#44aa22", + pointColor : "#44aa22", pointStrokeColor : "#fff", data : #{@charts[scope].success.to_json} } ] } var ctx = $("##{scope}Chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index 9550d71947..cf4285a267 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -1,22 +1,20 @@ - ci_project = @project.gitlab_ci_project -%fieldset - %legend Overall - %p +%h4 Overall stats +%ul + %li Total: %strong= pluralize ci_project.builds.count(:all), 'build' - %p + %li Successful: %strong= pluralize ci_project.builds.success.count(:all), 'build' - %p + %li Failed: %strong= pluralize ci_project.builds.failed.count(:all), 'build' - - %p + %li Success ratio: %strong #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}% - - %p + %li Commits covered: %strong = ci_project.commits.count(:all) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 4e0c3e5b3d..c03790aea0 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'head' -.gray-content-block +.gray-content-block.append-bottom-default .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs_commits' %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 6bbf15d05a..84ee843d9b 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'head' -.gray-content-block +.gray-content-block.append-bottom-default .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs' %ul.breadcrumb.repo-breadcrumb diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 915a4f526a..5ff7407c6f 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -60,7 +60,8 @@ module Ci class BuildTime < Chart def collect - commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30) + commits = project.commits.last(30) + commits.each do |commit| @labels << commit.short_sha @build_times << (commit.duration / 60) From b1a1cadcc2056540e4eb5a6b48c87b83656633be Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Nov 2015 11:51:58 +0100 Subject: [PATCH 433/443] Add changelog item Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d11076f484..fe521e0f14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.2.0 (unreleased) - Allow groups to appear in the search results if the group owner allows it - New design for project graphs page - Fix incoming email config defaults + - Improve Continuous Integration graphs page v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) From e8d97eb118ecbfe2a77abf2dcd56d7ab1cfac62a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 10 Nov 2015 12:01:29 +0100 Subject: [PATCH 434/443] Send build name and stage in CI notification e-mail --- CHANGELOG | 1 + app/views/ci/notify/build_fail_email.html.haml | 4 ++++ app/views/ci/notify/build_fail_email.text.erb | 2 ++ app/views/ci/notify/build_success_email.html.haml | 4 ++++ app/views/ci/notify/build_success_email.text.erb | 2 ++ 5 files changed, 13 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d11076f484..19850b58f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.2.0 (unreleased) - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page + - Send build name and stage in CI notification e-mail - Extend yml syntax for only and except to support specifying repository path - Enable shared runners to all new projects - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml index cefb75040e..b0aaea8907 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -12,6 +12,10 @@ Author: #{@build.commit.git_author_name} %p Branch: #{@build.ref} +%p + Stage: #{@build.stage} +%p + Job: #{@build.name} %p Message: #{@build.commit.git_commit_message} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb index 6de5dc10f1..17a3b9b1d3 100644 --- a/app/views/ci/notify/build_fail_email.text.erb +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml index 617b88f734..24c439e50e 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -13,6 +13,10 @@ Author: #{@build.commit.git_author_name} %p Branch: #{@build.ref} +%p + Stage: #{@build.stage} +%p + Job: #{@build.name} %p Message: #{@build.commit.git_commit_message} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb index d0a43ae1c1..bc8b978c3d 100644 --- a/app/views/ci/notify/build_success_email.text.erb +++ b/app/views/ci/notify/build_success_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> From 18cb430f7983ea557cf2308f5ea7c0af8b79a7b5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 10 Nov 2015 19:17:37 +0800 Subject: [PATCH 435/443] Replace CoffeeScript block into JavaScript in Views. For example view: shared/issuable/_context CoffeeScript: 190ms JavaScript: 19.7ms --- app/views/admin/labels/_form.html.haml | 4 +- app/views/ci/lints/show.html.haml | 16 +++--- .../groups/group_members/index.html.haml | 9 ++-- app/views/help/_shortcuts.html.haml | 8 +-- app/views/import/bitbucket/status.html.haml | 4 +- .../import/fogbugz/new_user_map.html.haml | 6 +-- app/views/import/fogbugz/status.html.haml | 4 +- app/views/import/github/status.html.haml | 4 +- app/views/import/gitlab/status.html.haml | 4 +- app/views/import/gitorious/status.html.haml | 4 +- app/views/import/google_code/status.html.haml | 4 +- app/views/layouts/_search.html.haml | 4 +- app/views/projects/_activity.html.haml | 4 +- app/views/projects/blob/_new_dir.html.haml | 2 +- app/views/projects/blob/_upload.html.haml | 6 +-- app/views/projects/buttons/_star.html.haml | 12 +++-- .../projects/commit/_commit_box.html.haml | 4 +- app/views/projects/graphs/commits.html.haml | 52 +++++++++---------- app/views/projects/graphs/show.html.haml | 21 ++++---- .../merge_requests/_new_compare.html.haml | 17 +++--- .../merge_requests/widget/_heading.html.haml | 7 +-- .../merge_requests/widget/_merged.html.haml | 27 +++++----- .../widget/open/_accept.html.haml | 11 ++-- .../widget/open/_check.html.haml | 8 +-- app/views/projects/new.html.haml | 14 ++--- .../projects/project_members/index.html.haml | 9 ++-- app/views/shared/issuable/_context.html.haml | 6 +-- app/views/shared/issuable/_filter.html.haml | 12 ++--- app/views/shared/projects/_list.html.haml | 4 +- app/views/users/show.html.haml | 4 +- 30 files changed, 155 insertions(+), 136 deletions(-) diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index ad58a3837f..a5ace4e7a3 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -31,5 +31,5 @@ = f.submit 'Save', class: 'btn btn-save js-save-button' = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' -:coffeescript - new Labels +:javascript + new Labels(); diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index a9b954771c..fb9057e488 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -11,15 +11,17 @@ .controls.pull-left.prepend-top-10 = submit_tag "Validate", class: 'btn btn-success submit-yml' - + %p.text-center.loading %i.fa.fa-refresh.fa-spin .results.prepend-top-20 -:coffeescript - $(".loading").hide() - $('form').bind 'ajax:beforeSend', -> - $(".loading").show() - $('form').bind 'ajax:complete', -> - $(".loading").hide() +:javascript + $(".loading").hide(); + $('form').bind('ajax:beforeSend', function() { + $(".loading").show(); + }); + $('form').bind('ajax:complete', function() { + $(".loading").hide(); + }); diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index fee4b0052b..15d289471c 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -36,7 +36,8 @@ = paginate @members, theme: 'gitlab' -:coffeescript - $('form.member-search-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '?' + $(@).serialize() +:javascript + $('form.member-search-form').on('submit', function(event) { + event.preventDefault(); + Turbolinks.visit(this.action + '?' + $(this).serialize()); + }); diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 67349fcbd7..7e801b5332 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -222,8 +222,8 @@ :javascript - $('.js-more-help-button').click(function(e){ - $(this).remove() - $('.hidden-shortcut').show() - e.preventDefault() + $('.js-more-help-button').click(function (e) { + $(this).remove()l + $('.hidden-shortcut').show(); + e.preventDefault(); }); diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 30bcdb8682..1f09a27e2d 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -66,5 +66,5 @@ again. -:coffeescript - new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") +:javascript + new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}"); diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index a701e49ac5..bc3c90294e 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -22,7 +22,7 @@ %strong Map a FogBugz account ID to a GitLab user %p Selecting a GitLab user will add a link to the GitLab user in the descriptions - of issues and comments (e.g. "By @johnsmith"). It will also + of issues and comments (e.g. "By @johnsmith"). It will also associate and/or assign these issues and comments with the selected user. .table-holder @@ -46,5 +46,5 @@ .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' -:coffeescript - new UsersSelect() +:javascript + new UsersSelect(); diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index beca6ab142..b902006597 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -48,5 +48,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}") +:javascript + new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}"); diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 0669b05adc..0699321c8c 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -43,5 +43,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") +:javascript + new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}"); diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 3bc85059e7..f4a2b33af2 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -43,5 +43,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") +:javascript + new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}"); diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 2e3a535737..71752d21ef 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -43,5 +43,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") +:javascript + new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index c5af06edf8..8c64fd27e6 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -67,5 +67,5 @@ = link_to "import flow", new_import_google_code_path again. -:coffeescript - new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") +:javascript + new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}"); diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index d1aa8f6246..a44f5762a6 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -25,6 +25,6 @@ :javascript $('.search-input').on('keyup', function(e) { if (e.keyCode == 27) { - $('.search-input').blur() + $('.search-input').blur(); } - }) + }); diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 012858f70b..101880bd10 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -8,5 +8,5 @@ .content_list{:"data-href" => activity_project_path(@project)} = spinner -:coffeescript - new Activities() +:javascript + new Activities(); diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index cb1567a2e6..a0fc8bbd75 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -21,5 +21,5 @@ = submit_tag "Create directory", class: 'btn btn-primary btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" -:coffeescript +:javascript disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create"); diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index e27f170752..a1c54e731f 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -26,6 +26,6 @@ = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" -:coffeescript - disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file' - new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}') +:javascript + disableButtonIfEmptyField($('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'); + new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}'); diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 3501dddefb..0658390203 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -4,11 +4,13 @@ %span.count = @project.star_count - :coffeescript - $('.project-home-panel .toggle-star').on 'ajax:success', (e, data, status, xhr) -> - $(@).replaceWith(data.html) - .on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') + :javascript + $('.project-home-panel .toggle-star').on('ajax:success', function (e, data, status, xhr) { + $(this).replaceWith(data.html); + }) + .on('ajax:error', function (e, xhr, status, error) { + new Flash('Star toggle failed. Try again later.', 'alert'); + }); - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a6458b8486..776768537d 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -55,5 +55,5 @@ %pre.commit-description = preserve(gfm(escape_once(@commit.description))) -:coffeescript - $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") +:javascript + $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 4e0c3e5b3d..eb33da7a5b 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -49,26 +49,24 @@ Commits per weekday %canvas#weekday-chart -:coffeescript - responsiveChart = (selector, data) -> - options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false } +:javascript + var responsiveChart = function (selector, data) { + var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }; + // get selector by context + var ctx = selector.get(0).getContext("2d"); + // pointing parent container to make chart.js inherit its width + var container = $(selector).parent(); + var generateChart = function() { + selector.attr('width', $(container).width()); + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; - # get selector by context - ctx = selector.get(0).getContext("2d") - # pointing parent container to make chart.js inherit its width - container = $(selector).parent() - - generateChart = -> - selector.attr('width', $(container).width()) - new Chart(ctx).Bar(data, options) - - # enabling auto-resizing - $(window).resize( generateChart ) - - generateChart() - - chartData = (keys, values) -> - data = { + var chartData = function (keys, values) { + var data = { labels : keys, datasets : [{ fillColor : "rgba(220,220,220,0.5)", @@ -78,13 +76,15 @@ barDatasetSpacing: 1, data : values }] - } + }; + return data; + }; - hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}) - responsiveChart($('#hour-chart'), hourData) + var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}); + responsiveChart($('#hour-chart'), hourData); - dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}) - responsiveChart($('#weekday-chart'), dayData) + var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}); + responsiveChart($('#weekday-chart'), dayData); - monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}) - responsiveChart($('#month-chart'), monthData) + var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); + responsiveChart($('#month-chart'), monthData); diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 6bbf15d05a..9e1feadeb2 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -28,18 +28,21 @@ -:coffeescript - $.ajax +:javascript + $.ajax({ type: "GET", url: location.href, - success: (data) -> - graph = new ContributorsStatGraph() - graph.init(data) + dataType: "json", + success: function (data) { + var graph = new ContributorsStatGraph(); + graph.init(data); - $("#brush_change").change -> - graph.change_date_header() - graph.redraw_authors() + $("#brush_change").change(function(){ + graph.change_date_header(); + graph.redraw_authors(); + }); $(".stat-graph").fadeIn(); $(".loading-graph").hide(); - dataType: "json" + } + }); diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 452006162d..d9eff1f932 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -77,12 +77,13 @@ }); -:coffeescript - - $(".merge-request-form").on 'submit', -> - if $("#merge_request_source_branch").val() is "" or $('#merge_request_target_branch').val() is "" - $(".mr-compare-errors").html("You must select source and target branch to proceed") - $(".mr-compare-errors").fadeIn() - event.preventDefault() - return +:javascript + $(".merge-request-form").on('submit', function () { + if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") { + $(".mr-compare-errors").html("You must select source and target branch to proceed"); + $(".mr-compare-errors").fadeIn(); + event.preventDefault(); + return; + } + }); diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index a3551516bf..ba5ad22bca 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -38,6 +38,7 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. - :coffeescript - $ -> - merge_request_widget.getCiStatus() + :javascript + $(function() { + merge_request_widget.getCiStatus(); + }); diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index f223f687de..a788fcea23 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -15,7 +15,7 @@ - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) .remove_source_branch_widget - %p + %p = succeed '.' do The changes were merged into %span.label-branch= @merge_request.target_branch @@ -25,7 +25,7 @@ Remove Source Branch .remove_source_branch_widget.failed.hide - %p + %p Failed to remove source branch '#{@merge_request.source_branch}'. .remove_source_branch_in_progress.hide @@ -33,17 +33,20 @@ = icon('spinner spin') Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload. - :coffeescript - $('.remove_source_branch').on 'click', -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').show() + :javascript + $('.remove_source_branch').on('click', function() { + $('.remove_source_branch_widget').hide(); + $('.remove_source_branch_in_progress').show(); + }); - $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> - location.reload() + $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) { + location.reload(); + }); - $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').hide() - $('.remove_source_branch_widget.failed').show() + $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) { + $('.remove_source_branch_widget').hide(); + $('.remove_source_branch_in_progress').hide(); + $('.remove_source_branch_widget.failed').show(); + }); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 689247f318..9b31014b58 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -20,8 +20,9 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true - :coffeescript - $('.accept-mr-form').on 'ajax:before', -> - btn = $('.accept_merge_request') - btn.disable() - btn.html(" Merge in progress") + :javascript + $('.accept-mr-form').on('ajax:before', function() { + var btn = $('.accept_merge_request'); + btn.disable(); + btn.html(" Merge in progress"); + }); diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index b6b8974297..e16878ba51 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -2,6 +2,8 @@ = icon("spinner spin") Checking ability to merge automatically… -:coffeescript - $ -> - merge_request_widget.getMergeStatus() +:javascript + $(function() { + merge_request_widget.getMergeStatus(); + }); + diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index daab2326bc..a02c12f06a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -124,9 +124,11 @@ Creating project & repository. %p Please wait a moment, this page will automatically refresh when ready. -:coffeescript - $('.how_to_import_link').bind 'click', (e) -> - e.preventDefault() - import_modal = $(this).next(".modal").show() - $('.modal-header .close').bind 'click', -> - $(".modal").hide() +:javascript + $('.how_to_import_link').bind('click', function (e) { + e.preventDefault(); + var import_modal = $(this).next(".modal").show(); + }); + $('.modal-header .close').bind('click', function() { + $(".modal").hide(); + }); diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 82809bec5b..9fc4be583c 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -29,7 +29,8 @@ - if @group = render "group_members", members: @group_members -:coffeescript - $('form.member-search-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '?' + $(@).serialize() +:javascript + $('form.member-search-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '?' + $(this).serialize()); + }); diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index cba18c1456..be66256c7b 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -45,6 +45,6 @@ .description-block.subscribed{class: ( 'hidden' unless subscribed )} You're receiving notifications because you're subscribed to this thread. -:coffeescript - new Subscription("#{toggle_subscription_path(issuable)}") - new IssuableContext() +:javascript + new Subscription("#{toggle_subscription_path(issuable)}"); + new IssuableContext(); diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e4e9c0987..d1231438ee 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -60,9 +60,9 @@ = hidden_field_tag :state_event, params[:state_event] = button_tag "Update issues", class: "btn update_selected_issues btn-save" -:coffeescript - new UsersSelect() - - $('form.filter-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '&' + $(@).serialize() +:javascript + new UsersSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 357cfd6a37..e5ffe1e29a 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -17,5 +17,5 @@ = link_to '#', class: 'js-expand' do Show all -:coffeescript - new ProjectsList() +:javascript + new ProjectsList(); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5a15c6c244..3099241218 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -115,5 +115,5 @@ projects: @projects.sort_by(&:star_count).reverse, projects_limit: 10, stars: true, avatar: true -:coffeescript - $(".user-calendar").load("#{user_calendar_path}") +:javascript + $(".user-calendar").load("#{user_calendar_path}"); From 99c05363ea8ff53cfa9620b626325773d17bb575 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Nov 2015 07:19:31 -0800 Subject: [PATCH 436/443] Remove CSS property preventing hard tabs from rendering in Chromium 45 This is to workaround a bug in Chromium 45 (https://code.google.com/p/chromium/issues/detail?id=446434), which is the default browser in Ubuntu 14.04 and older. Closes #3220 --- CHANGELOG | 1 + app/assets/stylesheets/framework/typography.scss | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 668ea87b28..c6bacab6a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index e6558a2385..ba0312ba0d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -173,7 +173,6 @@ * */ body { - text-rendering:optimizeLegibility; -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; } From 802b8fceb444f02fb8c2f67c5c433832baa33be7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Nov 2015 16:27:50 +0100 Subject: [PATCH 437/443] Fix graph description and text Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/graphs/ci/_builds.haml | 4 ++-- features/steps/project/graph.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 8424745540..8fca07114f 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -17,13 +17,13 @@ .prepend-top-default %p.light - Builds chart for last month + Builds for last month (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{height: 200} .prepend-top-default %p.light - Builds chart for last year + Builds for last year %canvas#yearChart.padded{height: 250} - [:week, :month, :year].each do |scope| diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 4abd5288d5..98f31f3b76 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -25,9 +25,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps step 'page should have CI graphs' do expect(page).to have_content 'Overall' - expect(page).to have_content 'Builds chart for last week' - expect(page).to have_content 'Builds chart for last month' - expect(page).to have_content 'Builds chart for last year' + expect(page).to have_content 'Builds for last week' + expect(page).to have_content 'Builds for last month' + expect(page).to have_content 'Builds for last year' expect(page).to have_content 'Commit duration in minutes for last 30 commits' end From 78966d4ca635506ee5323a88423275bc44d84a49 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Nov 2015 07:36:10 -0800 Subject: [PATCH 438/443] Bump rugged version to be in line with gitlab_git requirements --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1688ea4d9d..aae1ec7610 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -611,7 +611,7 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) - rugged (0.22.2) + rugged (0.23.3) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) From ffa8d84a6ef27dea6d87650390821d42f3d1b38a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Nov 2015 07:56:05 -0800 Subject: [PATCH 439/443] Bump charlock_holmes version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index aae1ec7610..dca8606806 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,7 +107,7 @@ GEM json (>= 1.7) celluloid (0.16.0) timers (~> 4.0.0) - charlock_holmes (0.6.9.4) + charlock_holmes (0.7.3) chunky_png (1.3.4) cliver (0.3.2) coderay (1.1.0) From b0452f26c0c3274d9d16c4b7e6f170737de4e31e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Nov 2015 08:16:33 -0800 Subject: [PATCH 440/443] Remove duplicate CHANGELOG etnries [ci skip] --- CHANGELOG | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 668ea87b28..168c4b50e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,8 +3,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses - Improved performance of replacing references in comments - Show last project commit to default branch on project home page From afcced01e579cb75d876c4612fc09e87d1502da6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 10 Nov 2015 13:15:18 -0500 Subject: [PATCH 441/443] Remove text-rendering property Closes #3220 [ci skip] --- app/assets/stylesheets/framework/layout.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index c7b3b60e76..b91c15d891 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -5,7 +5,6 @@ html { body { padding-top: $header-height; - text-rendering: geometricPrecision; } } From 5aa142212fc4fb7765aad801c1a25696fb752fba Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Nov 2015 00:27:01 -0800 Subject: [PATCH 442/443] Fix Drone CI service template not saving properly Closes #3419 --- CHANGELOG | 1 + app/models/project_services/drone_ci_service.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d82cab6f36..7567e25b89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) + - Fix Drone CI service template not saving properly (Stan Hu) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Improved performance of finding users by one of their Email addresses diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index c73c4b058a..c240213200 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -32,7 +32,6 @@ class DroneCiService < CiService def compose_service_hook hook = service_hook || build_service_hook - hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join hook.enable_ssl_verification = enable_ssl_verification hook.save end From ca25289b78e1b49148438831ad6bf165fa0ce56e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 10 Nov 2015 15:44:23 -0500 Subject: [PATCH 443/443] Update CHANGELOG [ci skip] --- CHANGELOG | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2b96a0a171..9cfcd23a9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,7 +34,9 @@ v 8.2.0 (unreleased) v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Prevent redirect loop when home_page_url is set to the root URL - - Ability to add release notes (markdown text and attachments) to git tags + - Fix incoming email config defaults + - Make color of "Accept Merge Request" button consistent with current build status + - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) v 8.1.3 - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) @@ -42,7 +44,7 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.1 +v 8.1.2 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility @@ -52,6 +54,9 @@ v 8.1.1 - Fix CI badge - Allow developer to manage builds +v 8.1.1 + - Removed, see 8.1.2 + v 8.1.0 - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu) - Fix duplicate repositories in GitHub import page (Stan Hu)

    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 z%0A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 Date: Tue, 3 Nov 2015 17:58:26 +0100 Subject: [PATCH 357/443] Use proper labels for OAuth providers --- lib/gitlab/o_auth/provider.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb index 90c3fe8da3..9ad7a38d50 100644 --- a/lib/gitlab/o_auth/provider.rb +++ b/lib/gitlab/o_auth/provider.rb @@ -1,6 +1,12 @@ module Gitlab module OAuth class Provider + LABELS = { + "github" => "GitHub", + "gitlab" => "GitLab.com", + "google_oauth2" => "Google" + }.freeze + def self.providers Devise.omniauth_providers end @@ -23,8 +29,9 @@ module Gitlab end def self.label_for(name) + name = name.to_s config = config_for(name) - (config && config['label']) || name.to_s.titleize + (config && config['label']) || LABELS[name] || name.titleize end end end From 05eb9e7884a1a1eb4144f84e7f586d26b011f4f1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 3 Nov 2015 16:16:15 -0500 Subject: [PATCH 358/443] Minor reformatting for Facebook integration doc [ci skip] --- doc/integration/facebook.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index 5880b5baf7..bc1f167308 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -19,7 +19,7 @@ something else descriptive. 1. Enter the address of your GitLab installation at the bottom of the package - ![Facebook Website URL](facebook_website_url.png) + ![Facebook Website URL](facebook_website_url.png) 1. Choose "Next" @@ -28,6 +28,7 @@ something else descriptive. 1. Choose "Settings" in the menu on the left 1. Fill in a contact email for your app + ![Facebook App Settings](facebook_app_settings.png) 1. Choose "Save Changes" @@ -51,15 +52,15 @@ something else descriptive. For omnibus package: ```sh - sudo editor /etc/gitlab/gitlab.rb + sudo editor /etc/gitlab/gitlab.rb ``` For installations from source: ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` 1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. @@ -69,20 +70,20 @@ something else descriptive. For omnibus package: ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "facebook", - "app_id" => "YOUR_APP_ID", - "app_secret" => "YOUR_APP_SECRET" - } - ] + gitlab_rails['omniauth_providers'] = [ + { + "name" => "facebook", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET" + } + ] ``` For installations from source: ``` - - { name: 'facebook', app_id: 'YOUR_APP_ID', - app_secret: 'YOUR_APP_SECRET' } + - { name: 'facebook', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } ``` 1. Change 'YOUR_APP_ID' to the API key from Facebook page in step 10. From 312375ac7c7d6619740899cd185a8dde1d653955 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 3 Nov 2015 17:10:17 -0500 Subject: [PATCH 359/443] Update Shell Commands doc for configurable git binary path --- doc/development/shell_commands.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 2d1d0fb415..65cdd74bdb 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -35,6 +35,16 @@ Gitlab::Popen.popen(%W(find /some/path -not -path /some/path -mmin +120 -delete) This coding style could have prevented CVE-2013-4490. +## Always use the configurable git binary path for git commands + +```ruby +# Wrong +system(*%W(git branch -d -- #{branch_name})) + +# Correct +system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name})) +``` + ## Bypass the shell by splitting commands into separate tokens When we pass shell commands as a single string to Ruby, Ruby will let `/bin/sh` evaluate the entire string. Essentially, we are asking the shell to evaluate a one-line script. This creates a risk for shell injection attacks. It is better to split the shell command into tokens ourselves. Sometimes we use the scripting capabilities of the shell to change the working directory or set environment variables. All of this can also be achieved securely straight from Ruby @@ -81,9 +91,9 @@ In the GitLab codebase, we avoid the option/argument ambiguity by _always_ using ```ruby # Wrong -system(*%W(git branch -d #{branch_name})) +system(*%W(#{Gitlab.config.git.bin_path} branch -d #{branch_name})) # Correct -system(*%W(git branch -d -- #{branch_name})) +system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name})) ``` This coding style could have prevented CVE-2013-4582. @@ -94,9 +104,9 @@ Capturing the output of shell commands with backticks reads nicely, but you are ```ruby # Wrong -logs = `cd #{repo_dir} && git log` +logs = `cd #{repo_dir} && #{Gitlab.config.git.bin_path} log` # Correct -logs, exit_status = Gitlab::Popen.popen(%W(git log), repo_dir) +logs, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} log), repo_dir) # Wrong user = `whoami` @@ -108,7 +118,7 @@ In other repositories, such as gitlab-shell you can also use `IO.popen`. ```ruby # Safe IO.popen example -logs = IO.popen(%W(git log), chdir: repo_dir) { |p| p.read } +logs = IO.popen(%W(#{Gitlab.config.git.bin_path} log), chdir: repo_dir) { |p| p.read } ``` Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error. From d09d62b6b875102b7a334fcf9e689537e1f25013 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 3 Nov 2015 17:10:38 -0500 Subject: [PATCH 360/443] Replace all usages of `git` command with configurable binary path Closes #3311 --- app/models/repository.rb | 12 ++++++------ config/initializers/2_app.rb | 6 +++--- lib/backup/repository.rb | 6 +++--- lib/gitlab/force_push_check.rb | 2 +- lib/gitlab/git_ref_validator.rb | 2 +- lib/gitlab/upgrader.rb | 8 ++++---- lib/tasks/gitlab/check.rake | 2 +- lib/tasks/gitlab/shell.rake | 10 +++++----- spec/models/project_wiki_spec.rb | 2 +- spec/requests/api/repositories_spec.rb | 4 ++-- spec/support/test_env.rb | 8 ++++---- 11 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 9266ba27f0..f8c4cb1387 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -89,7 +89,7 @@ class Repository def find_commits_by_message(query) # Limited to 1000 commits for now, could be parameterized? - args = %W(git log --pretty=%H --max-count 1000 --grep=#{query}) + args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query}) git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) commits = git_log_results.map { |c| commit(c) } @@ -296,7 +296,7 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(git rev-list --max-count=1 #{sha} -- #{path}) + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) sha = Gitlab::Popen.popen(args, path_to_repo).first.strip commit(sha) end @@ -347,7 +347,7 @@ class Repository end def branch_names_contains(sha) - args = %W(git branch --contains #{sha}) + args = %W(#{Gitlab.config.git.bin_path} branch --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first if names.respond_to?(:split) @@ -364,7 +364,7 @@ class Repository end def tag_names_contains(sha) - args = %W(git tag --contains #{sha}) + args = %W(#{Gitlab.config.git.bin_path} tag --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first if names.respond_to?(:split) @@ -505,7 +505,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -537,7 +537,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(git fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb index 688cdf5f4b..35b150c992 100644 --- a/config/initializers/2_app.rb +++ b/config/initializers/2_app.rb @@ -1,8 +1,8 @@ module Gitlab - VERSION = File.read(Rails.root.join("VERSION")).strip - REVISION = Gitlab::Popen.popen(%W(git log --pretty=format:%h -n 1)).first.chomp - def self.config Settings end + + VERSION = File.read(Rails.root.join("VERSION")).strip + REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 4d70f7883d..a82a7e1f7b 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -35,7 +35,7 @@ module Backup if wiki.repository.empty? $progress.puts " [SKIPPED]".cyan else - cmd = %W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? $progress.puts " [DONE]".green @@ -67,7 +67,7 @@ module Backup FileUtils.mkdir_p(path_to_repo(project)) cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)}) else - cmd = %W(git init --bare #{path_to_repo(project)}) + cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_repo(project)}) end if system(*cmd, silent) @@ -87,7 +87,7 @@ module Backup # that was initialized with ProjectWiki.new() and then # try to restore with 'git clone --bare'. FileUtils.rm_rf(path_to_repo(wiki)) - cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) + cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) if system(*cmd, silent) $progress.puts " [DONE]".green diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb index fdb6a35c78..93c6a5bb7f 100644 --- a/lib/gitlab/force_push_check.rb +++ b/lib/gitlab/force_push_check.rb @@ -7,7 +7,7 @@ module Gitlab if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) false else - missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) + missed_refs, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) missed_refs.split("\n").size > 0 end end diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 39d17def93..4d83d8e72a 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -6,7 +6,7 @@ module Gitlab # Returns true for a valid reference name, false otherwise def validate(ref_name) Gitlab::Utils.system_silent( - %W(git check-ref-format refs/#{ref_name})) + %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name})) end end end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index cf040971c6..f3567f3ef8 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -50,15 +50,15 @@ module Gitlab end def fetch_git_tags - remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git)) + remote_tags, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git)) remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) end def update_commands { - "Stash changed files" => %W(git stash), - "Get latest code" => %W(git fetch), - "Switch to new version" => %W(git checkout v#{latest_version}), + "Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash), + "Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch), + "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}), "Install gems" => %W(bundle), "Migrate DB" => %W(bundle exec rake db:migrate), "Recompile assets" => %W(bundle exec rake assets:clean assets:precompile), diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2e73f792a9..a25fac62cf 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -824,7 +824,7 @@ namespace :gitlab do repo_dirs = Dir.glob(File.join(namespace_dir, '*')) repo_dirs.each do |dir| puts "\nChecking repo at #{dir}" - system(*%w(git fsck), chdir: dir) + system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: dir) end end end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 3c0cc763d1..dd61632e55 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -17,7 +17,7 @@ namespace :gitlab do # Clone if needed unless File.directory?(target_dir) - system(*%W(git clone -- #{args.repo} #{target_dir})) + system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir})) end # Make sure we're on the right tag @@ -27,7 +27,7 @@ namespace :gitlab do reseted = reset_to_commit(args) unless reseted - system(*%W(git fetch origin)) + system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) reset_to_commit(args) end @@ -128,14 +128,14 @@ namespace :gitlab do end def reset_to_commit(args) - tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag})) + tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- #{args.tag})) unless status.zero? - tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag})) + tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- origin/#{args.tag})) end tag = tag.strip - system(*%W(git reset --hard #{tag})) + system(*%W(#{Gitlab.config.git.bin_path} reset --hard #{tag})) end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 94802dcfb7..9f6cdeeaa9 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -223,7 +223,7 @@ describe ProjectWiki do def create_temp_repo(path) FileUtils.mkdir_p path - system(*%W(git init --quiet --bare -- #{path})) + system(*%W(#{Gitlab.config.git.bin_path} init --quiet --bare -- #{path})) end def remove_temp_repo(path) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 1149f7e798..faf6b77a46 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -36,8 +36,8 @@ describe API::API, api: true do it 'should create a new annotated tag' do # Identity must be set in .gitconfig to create annotated tag. repo_path = project.repository.path_to_repo - system(*%W(git --git-dir=#{repo_path} config user.name #{user.name})) - system(*%W(git --git-dir=#{repo_path} config user.email #{user.email})) + system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name})) + system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email})) post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v7.1.0', diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index d12ba25b71..787670e929 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -96,15 +96,15 @@ module TestEnv clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git" unless File.directory?(repo_path) - system(*%W(git clone -q #{clone_url} #{repo_path})) + system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end Dir.chdir(repo_path) do branch_sha.each do |branch, sha| # Try to reset without fetching to avoid using the network. - reset = %W(git update-ref refs/heads/#{branch} #{sha}) + reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) unless system(*reset) - if system(*%w(git fetch origin)) + if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) unless system(*reset) raise 'The fetched test seed '\ 'does not contain the required revision.' @@ -117,7 +117,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(git_env, *%W(git clone -q --bare #{repo_path} #{repo_path_bare})) + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) end def copy_repo(project) From 5f94e130d11adf4398058d1c558de6f44b11675a Mon Sep 17 00:00:00 2001 From: mozillazg Date: Wed, 4 Nov 2015 11:41:44 +0800 Subject: [PATCH 361/443] update example of regex for pytest-cov --- app/views/projects/ci_settings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml index d711413c6b..20bdccc902 100644 --- a/app/views/projects/ci_settings/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -102,7 +102,7 @@ %code \(\d+.\d+\%\) covered %li pytest-cov (Python) - - %code \d+\%$ + %code \d+\%\s*$ From 91cbf9db0c5c95381e7d422fd684e623d58fadab Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 26 Oct 2015 07:26:03 +0100 Subject: [PATCH 362/443] Add allow_failure field to commit status API Closes #3196 --- CHANGELOG | 1 + doc/api/commits.md | 6 ++++-- lib/api/entities.rb | 2 +- spec/requests/api/commit_status_spec.rb | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 16055208db..e7238c4566 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses + - Add allow_failure field to commit status API (Stan Hu) - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL diff --git a/doc/api/commits.md b/doc/api/commits.md index 9f72adc6ed..8e4a0ee1b8 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -22,7 +22,8 @@ Parameters: "author_name": "Dmitriy Zaporozhets", "author_email": "dzaporozhets@sphereconsultinginc.com", "created_at": "2012-09-20T11:50:22+03:00", - "message": "Replace sanitize with escape once" + "message": "Replace sanitize with escape once", + "allow_failure": false }, { "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", @@ -31,7 +32,8 @@ Parameters: "author_name": "randx", "author_email": "dmitriy.zaporozhets@gmail.com", "created_at": "2012-09-20T09:06:12+03:00", - "message": "Sanitize for network graph" + "message": "Sanitize for network graph", + "allow_failure": false } ] ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 883a5e14b1..20cadae229 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -231,7 +231,7 @@ module API class CommitStatus < Grape::Entity expose :id, :sha, :ref, :status, :name, :target_url, :description, - :created_at, :started_at, :finished_at + :created_at, :started_at, :finished_at, :allow_failure expose :author, using: Entities::UserBasic end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index b9e6dfc15a..a28607bd24 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -18,7 +18,7 @@ describe API::API, api: true do before do @status1 = create(:commit_status, commit: ci_commit, status: 'running') @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending') - @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running') + @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true) @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success') @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success') @status6 = create(:commit_status, commit: ci_commit, status: 'success') @@ -30,6 +30,8 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) + json_response.sort_by!{ |status| status['id'] } + expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) end it "should return all commit statuses" do From bdb45360e76a272c275690cb11227a737c56e934 Mon Sep 17 00:00:00 2001 From: Tim Jabs Date: Thu, 8 Oct 2015 18:06:51 +0200 Subject: [PATCH 363/443] Changed documentation of converting a MySQL-Database with Gitlab to a Postgresdatatabase. The instructions were missleading. See -> https://gitlab.com/gitlab-org/gitlab-ce/issues/2904 --- doc/update/mysql_to_postgresql.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index a596ea3845..a7de5648c0 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -60,6 +60,9 @@ sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_produc sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed # Compress database backup +# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with: +# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql +# The compressed databasedump is not supported at 7.12.0 and older. sudo -u git -H gzip db/database.sql # Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar. @@ -71,4 +74,5 @@ sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz # Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab # installation. +# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups. ``` From c5d637b2cad3ad760f54d4aaa398847b4e251524 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Nov 2015 14:13:25 +0100 Subject: [PATCH 364/443] Improvements to profile page UI * add separator between tabs * show project avatars * fix tooltip offset on user calendar * remove gray hover for tabs Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/calendar.js.coffee | 2 +- app/assets/stylesheets/framework/common.scss | 22 +++++++++++++++++++- app/assets/stylesheets/framework/mixins.scss | 1 + app/assets/stylesheets/pages/profile.scss | 4 ---- app/views/users/show.html.haml | 6 +++--- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 2b1e20d322..9762123692 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -25,7 +25,7 @@ class @Calendar 30 ] legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 76 + cellSize: $('.user-calendar').width() / 73 onClick: (date, count) -> formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3d0b71e066..41287d52f6 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -389,12 +389,32 @@ table { .center-middle-menu { @include nav-menu; + padding: 0; text-align: center; margin: -$gl-padding; - height: auto; margin-top: 0; margin-bottom: 0; + height: 58px; border-bottom: 1px solid $border-color; + + li { + &:after { + content: "|"; + color: $border-gray-light; + } + + &:last-child { + &:after { + content: none; + } + } + + > a { + display: inline-block; + text-transform: uppercase; + font-size: 13px; + } + } } .dropzone .dz-preview .dz-progress { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index fe078d016d..b9c179f288 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -137,6 +137,7 @@ &:hover, &:active, &:focus { text-decoration: none; + outline: none; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index bc1ad21305..1d6ca0dfc1 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -75,7 +75,3 @@ text-decoration: none; } } - -.cal-heatmap-container { - margin: 0 auto; -} diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index e22d93aae8..5a15c6c244 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -73,7 +73,7 @@ .user-calendar-activities -%ul.nav.center-middle-menu +%ul.center-middle-menu %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity @@ -106,14 +106,14 @@ .contributed-projects = render 'shared/projects/list', projects: @contributed_projects.sort_by(&:star_count).reverse, - projects_limit: 5, stars: true, avatar: false + projects_limit: 5, stars: true, avatar: true - if @projects.present? .tab-pane#personal .personal-projects = render 'shared/projects/list', projects: @projects.sort_by(&:star_count).reverse, - projects_limit: 10, stars: true, avatar: false + projects_limit: 10, stars: true, avatar: true :coffeescript $(".user-calendar").load("#{user_calendar_path}") From 8508be74d2063bb86e39d1cc8667e8adb9dfe63e Mon Sep 17 00:00:00 2001 From: Kjel Delaey Date: Wed, 4 Nov 2015 15:55:19 +0100 Subject: [PATCH 365/443] Style inline event items with titles containing long words properly.. On a tablet in portrait mode with a width of 768px the event-title isn't aligned properly when the title contains a long string. This also happens when resizing your browser viewport on a desktop. Example string: Administrator pushed new branch feature-branch-with-a-very-long-name at Gitlab Org / Gitlab Test In the UI it would look like the example below: ---------- | | | AVATAR | less than a minute ago | | ---------- Administrator pushed new branch feature-branch-with-a-very-long-name at... --- app/assets/stylesheets/pages/events.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index dfb901652b..d2ca106cc8 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -11,9 +11,12 @@ color: #7f8fa4; &.event-inline { + padding-left: $gl-padding + $gl-avatar-size; + .avatar { position: relative; top: -2px; + margin-left: -$gl-avatar-size; } .event-title { @@ -155,6 +158,10 @@ @media (max-width: $screen-xs-max) { .event-item { + &.event-inline { + padding-left: $gl-padding; + } + .event-title { white-space: normal; overflow: visible; From 8b0906c86d10ff02865e99400f2c0e3bdd1ac6fe Mon Sep 17 00:00:00 2001 From: Kjel Delaey Date: Wed, 4 Nov 2015 16:22:27 +0100 Subject: [PATCH 366/443] Apply the same rules to block event items.. Try to avoid alignment issues as well. --- app/assets/stylesheets/pages/events.scss | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index d2ca106cc8..02401c7c73 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,19 +4,16 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding; + padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size); margin-left: -$gl-padding; margin-right: -$gl-padding; border-bottom: 1px solid $table-border-color; color: #7f8fa4; &.event-inline { - padding-left: $gl-padding + $gl-avatar-size; - .avatar { position: relative; top: -2px; - margin-left: -$gl-avatar-size; } .event-title { @@ -33,6 +30,7 @@ } .avatar { + margin-left: -$gl-avatar-size; margin-right: 15px; } @@ -46,9 +44,6 @@ } .event-body { - margin-left: 63px; - margin-right: 80px; - .event-note { margin-top: 5px; word-wrap: break-word; @@ -158,9 +153,7 @@ @media (max-width: $screen-xs-max) { .event-item { - &.event-inline { - padding-left: $gl-padding; - } + padding-left: $gl-padding; .event-title { white-space: normal; From 3f2955de3bf08ef1ec378d323fb2360547a4ca16 Mon Sep 17 00:00:00 2001 From: Kjel Delaey Date: Wed, 4 Nov 2015 16:23:39 +0100 Subject: [PATCH 367/443] Combine selectors because the same style is being applied --- app/assets/stylesheets/pages/events.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 02401c7c73..f8d1afda73 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -16,10 +16,7 @@ top: -2px; } - .event-title { - line-height: 44px; - } - + .event-title, .event-item-timestamp { line-height: 44px; } From 530c9519d4d0760848daf5026f93fbcbff348656 Mon Sep 17 00:00:00 2001 From: Kjel Delaey Date: Wed, 4 Nov 2015 16:40:22 +0100 Subject: [PATCH 368/443] Make sure that multi-commit rows inside event bodies are aligned properly.. Prevent the second, third, ... rows from having a different indentation than the first commit row. Adding the extra "15px padding" to the event item prevents this. Having a 15px margin on the avatar only doesn't prevent this from happening. --- app/assets/stylesheets/pages/events.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index f8d1afda73..c150b4ac19 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,7 +4,7 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size); + padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px); margin-left: -$gl-padding; margin-right: -$gl-padding; border-bottom: 1px solid $table-border-color; @@ -27,8 +27,7 @@ } .avatar { - margin-left: -$gl-avatar-size; - margin-right: 15px; + margin-left: -($gl-avatar-size + 15px); } .event-title { From 07821839d0616c06180388b91e3131bc008522a0 Mon Sep 17 00:00:00 2001 From: mutec Date: Wed, 4 Nov 2015 17:49:23 +0100 Subject: [PATCH 369/443] Update installation.md --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 599d2541f2..e31448263c 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -332,7 +332,7 @@ GitLab Shell is an SSH access and repository management software developed speci # Go to Gitlab installation folder - cd /home/git/gilab + cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production From 95da91a66de5820a44989aa708338c24177cd10c Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Wed, 4 Nov 2015 17:07:21 +0000 Subject: [PATCH 370/443] Use single spaces --- doc_styleguide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc_styleguide.md b/doc_styleguide.md index 656bb1d17f..cceb449a85 100644 --- a/doc_styleguide.md +++ b/doc_styleguide.md @@ -15,6 +15,8 @@ For subtitles, use '##', '###' and so on. - Do not duplicate information. - Be brief and clear. - Whenever it applies, add documents in alphabetical order. +- Write in US English +- Use [single spaces](http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html) instead of double spaces. ## Images From dfccc28e38b6298eff8eeedf1ba91a93c0182353 Mon Sep 17 00:00:00 2001 From: Vincent Lequertier Date: Wed, 4 Nov 2015 19:04:20 +0100 Subject: [PATCH 371/443] It makes more sense like this --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 599d2541f2..5a09cf7c96 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -253,8 +253,8 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da nproc # Enable cluster mode if you expect to have a high load instance - # Ex. change amount of workers to 3 for 2GB RAM server # Set the number of workers to at least the number of cores + # Ex. change amount of workers to 3 for 2GB RAM server sudo -u git -H editor config/unicorn.rb # Copy the example Rack attack config From b6f89c3490f8c31607073c9761de3ebb30ae244e Mon Sep 17 00:00:00 2001 From: Kjel Delaey Date: Thu, 5 Nov 2015 09:56:51 +0100 Subject: [PATCH 372/443] Add right margin to event-body.. Otherwise text will flow under the "timeago" element. The original value was 80px (see commit 8b0906c8), but a value of 174px makes more sense. (see event-title `calc` function). --- app/assets/stylesheets/pages/events.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index c150b4ac19..282aaf2219 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -40,6 +40,8 @@ } .event-body { + margin-right: 174px; + .event-note { margin-top: 5px; word-wrap: break-word; From 33b8f002636ad6171637108b53732c74d90b14ad Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 10:51:12 +0100 Subject: [PATCH 373/443] Add edit/update tag actions for future release notes Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/tags_controller.rb | 8 ++++++++ app/views/projects/tags/_tag.html.haml | 2 ++ app/views/projects/tags/edit.html.haml | 0 config/routes.rb | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/tags/edit.html.haml diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index f565fbbbbc..a30c284c41 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -10,6 +10,14 @@ class Projects::TagsController < Projects::ApplicationController @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) end + def edit + # TODO: implement + end + + def update + # TODO: implement + end + def create result = CreateTagService.new(@project, current_user). execute(params[:tag_name], params[:ref], params[:message]) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2ca295fc5f..887f3ab075 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -9,6 +9,8 @@   = strip_gpg_signature(tag.message) .controls + = link_to edit_namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do + = icon("pencil") - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/tags/edit.html.haml b/app/views/projects/tags/edit.html.haml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/routes.rb b/config/routes.rb index 0458f538eb..8dc167cbbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -569,7 +569,7 @@ Gitlab::Application.routes.draw do end resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :tags, constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] From 1c4d1c3bd69a6f9ec43cce4ab59de4ba47f73229 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 11:03:02 +0100 Subject: [PATCH 374/443] Add release model Signed-off-by: Dmitriy Zaporozhets --- app/models/release.rb | 2 ++ db/migrate/20151105094515_create_releases.rb | 14 ++++++++++++++ db/schema.rb | 13 ++++++++++++- spec/factories/releases.rb | 9 +++++++++ spec/models/release_spec.rb | 5 +++++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 app/models/release.rb create mode 100644 db/migrate/20151105094515_create_releases.rb create mode 100644 spec/factories/releases.rb create mode 100644 spec/models/release_spec.rb diff --git a/app/models/release.rb b/app/models/release.rb new file mode 100644 index 0000000000..1dc9ce6dd4 --- /dev/null +++ b/app/models/release.rb @@ -0,0 +1,2 @@ +class Release < ActiveRecord::Base +end diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb new file mode 100644 index 0000000000..fe4608c666 --- /dev/null +++ b/db/migrate/20151105094515_create_releases.rb @@ -0,0 +1,14 @@ +class CreateReleases < ActiveRecord::Migration + def change + create_table :releases do |t| + t.string :tag + t.text :description + t.integer :project_id + + t.timestamps + end + + add_index :releases, :project_id + add_index :releases, [:project_id, :tag] + end +end diff --git a/db/schema.rb b/db/schema.rb index 73fc83c3d6..3444902447 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: 20151026182941) do +ActiveRecord::Schema.define(version: 20151105094515) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -637,6 +637,17 @@ ActiveRecord::Schema.define(version: 20151026182941) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree + create_table "releases", force: true do |t| + t.string "tag" + t.text "description" + t.integer "project_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree + add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree + create_table "sent_notifications", force: true do |t| t.integer "project_id" t.integer "noteable_id" diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb new file mode 100644 index 0000000000..067d8138e4 --- /dev/null +++ b/spec/factories/releases.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :release do + tag "MyString" + description "MyText" + project_id 1 + end +end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb new file mode 100644 index 0000000000..e533734ba0 --- /dev/null +++ b/spec/models/release_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Release, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From ba67af79a9ec0d37d08e51af034dd6c21170713c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 11:16:41 +0100 Subject: [PATCH 375/443] More release related logic to separate resource Signed-off-by: Dmitriy Zaporozhets --- .../projects/releases_controller.rb | 32 +++++++++++++++++++ app/controllers/projects/tags_controller.rb | 8 ----- app/models/project.rb | 3 +- app/views/projects/releases/edit.html.haml | 6 ++++ app/views/projects/releases/show.html.haml | 1 + app/views/projects/tags/_tag.html.haml | 2 +- app/views/projects/tags/edit.html.haml | 0 config/routes.rb | 5 ++- 8 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 app/controllers/projects/releases_controller.rb create mode 100644 app/views/projects/releases/edit.html.haml create mode 100644 app/views/projects/releases/show.html.haml delete mode 100644 app/views/projects/tags/edit.html.haml diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb new file mode 100644 index 0000000000..877cc0f367 --- /dev/null +++ b/app/controllers/projects/releases_controller.rb @@ -0,0 +1,32 @@ +class Projects::ReleasesController < Projects::ApplicationController + # Authorize + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code! + before_action :tag + before_action :release + + def show + end + + def edit + end + + def update + description = params[:release][:description] + release.update_attributes(description: description) + release.save + + redirect_to namespace_project_tag_release_path(@project.namespace, @project, @tag.name) + end + + private + + def tag + @tag ||= @repository.find_tag(params[:tag_id]) + end + + def release + @release ||= @project.releases.find_or_initialize_by(tag: @tag.name) + end +end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index a30c284c41..f565fbbbbc 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -10,14 +10,6 @@ class Projects::TagsController < Projects::ApplicationController @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) end - def edit - # TODO: implement - end - - def update - # TODO: implement - end - def create result = CreateTagService.new(@project, current_user). execute(params[:tag_name], params[:ref], params[:message]) diff --git a/app/models/project.rb b/app/models/project.rb index 74b89aad49..cd3de01a95 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -121,6 +121,7 @@ class Project < ActiveRecord::Base has_many :starrers, through: :users_star_projects, source: :user has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :releases, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id @@ -247,7 +248,7 @@ class Project < ActiveRecord::Base joins(:namespace). iwhere('namespaces.path' => namespace_path) - projects.where('projects.path' => project_path).take || + projects.where('projects.path' => project_path).take || projects.iwhere('projects.path' => project_path).take end diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml new file mode 100644 index 0000000000..3b4a5e7223 --- /dev/null +++ b/app/views/projects/releases/edit.html.haml @@ -0,0 +1,6 @@ += form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form' }) do |f| + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: f, attr: :description, classes: 'js-quick-submit' + = render 'projects/notes/hints' + .error-alert + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/releases/show.html.haml b/app/views/projects/releases/show.html.haml new file mode 100644 index 0000000000..bd83150008 --- /dev/null +++ b/app/views/projects/releases/show.html.haml @@ -0,0 +1 @@ += debug @release diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 887f3ab075..526dd3f580 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -9,7 +9,7 @@   = strip_gpg_signature(tag.message) .controls - = link_to edit_namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do = icon("pencil") - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' diff --git a/app/views/projects/tags/edit.html.haml b/app/views/projects/tags/edit.html.haml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/config/routes.rb b/config/routes.rb index 8dc167cbbd..5836da47eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -569,7 +569,10 @@ Gitlab::Application.routes.draw do end resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, constraints: { id: Gitlab::Regex.git_reference_regex } + resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do + resource :release + end + resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] From a4d75e3aec2e721231bc1e01a2e5e87aefe15113 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 12:15:25 +0100 Subject: [PATCH 376/443] Add ability to edit and show release notes Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/dispatcher.js.coffee | 3 ++ app/assets/stylesheets/framework/common.scss | 1 + .../projects/releases_controller.rb | 1 + app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/commits/_head.html.haml | 2 +- app/views/projects/releases/edit.html.haml | 24 +++++++++---- app/views/projects/releases/show.html.haml | 36 ++++++++++++++++++- 7 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 5bf0b30217..030826be74 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -39,6 +39,9 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new DropzoneInput($('.merge-request-form')) new IssuableForm($('.merge-request-form')) + when 'projects:releases:edit' + new ZenMode() + new DropzoneInput($('.release-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssuable() diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 41287d52f6..ddbacd7fd4 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -16,6 +16,7 @@ .append-bottom-10 { margin-bottom:10px } .append-bottom-15 { margin-bottom:15px } .append-bottom-20 { margin-bottom:20px } +.append-bottom-default { margin-bottom: $gl-padding; } .inline { display: inline-block } .center { text-align: center } diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 877cc0f367..7d1a011cc0 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController before_action :release def show + @commit = @repository.commit(@tag.target) end def edit diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 20db2866d1..2b91d7721f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -32,7 +32,7 @@ Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches)) do + = nav_link(controller: %w(commit commits compare repositories tags branches releases)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index a849bf8469..f11a41cfd7 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -12,7 +12,7 @@ Branches %span.badge.js-totalbranch-count= @repository.branches.size - = nav_link(controller: :tags) do + = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 3b4a5e7223..612e469622 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,6 +1,18 @@ -= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form' }) do |f| - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'js-quick-submit' - = render 'projects/notes/hints' - .error-alert - = f.submit 'Save changes', class: 'btn btn-save' += render "projects/commits/header_title" += render "projects/commits/head" + +.gray-content-block + .oneline + Release notes for #{@tag.name} + +.prepend-top-default + = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f| + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit' + = render 'projects/notes/hints' + .error-alert + .prepend-top-default + = f.submit 'Save changes', class: 'btn btn-save' + - if @release.persisted? + = link_to "Cancel", namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + diff --git a/app/views/projects/releases/show.html.haml b/app/views/projects/releases/show.html.haml index bd83150008..606510f132 100644 --- a/app/views/projects/releases/show.html.haml +++ b/app/views/projects/releases/show.html.haml @@ -1 +1,35 @@ -= debug @release +- page_title @release.tag, "Releases" += render "projects/commits/header_title" += render "projects/commits/head" + +.gray-content-block + .pull-right + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn' do + = icon("pencil") + .oneline Release notes for #{@tag.name} + +.append-bottom-default.prepend-top-default + - if @release.description.present? + .description + .wiki + = preserve do + = markdown @release.description + - else + This tag has no release notes yet. Press edit button to add one + +.gray-content-block.middle-block.clearfix + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + Browse code + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + Commits + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @tag.name, btn_class: 'btn-grouped' + -#- if can?(current_user, :admin_project, @project) + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + %i.fa.fa-trash-o + +.gray-content-block.second-block + - if @commit + = render 'projects/commits/commit', commit: @commit, project: @project + - else + Cant find HEAD commit for this tag From 6051c28fc03b4d9928ee2f2855f210845f9c0579 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 5 Nov 2015 12:38:00 +0200 Subject: [PATCH 377/443] Allow groups to appear in the search results if the group owner allows it --- CHANGELOG | 1 + app/controllers/groups_controller.rb | 6 +- app/finders/groups_finder.rb | 57 ++++++++++--------- app/helpers/search_helper.rb | 2 +- app/models/group.rb | 2 +- app/views/groups/edit.html.haml | 9 +++ .../20151103001141_add_public_to_group.rb | 5 ++ db/schema.rb | 9 +-- spec/finders/group_finder_spec.rb | 15 +++++ spec/helpers/search_helper_spec.rb | 5 ++ spec/models/group_spec.rb | 19 +++++++ 11 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 db/migrate/20151103001141_add_public_to_group.rb create mode 100644 spec/finders/group_finder_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 0ec6030b13..3a75f50e9a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.2.0 (unreleased) - Include commit logs in project search - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from + - Allow groups to appear in the search results if the group owner allows it v 8.1.3 - Spread out runner contacted_at updates diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 40fb15a5b3..fb4eb094f2 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -4,12 +4,12 @@ class GroupsController < Groups::ApplicationController before_action :group, except: [:new, :create] # Authorize - before_action :authorize_read_group!, except: [:show, :new, :create] + before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_action :load_projects, except: [:new, :create, :projects, :edit, :update] + before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete] before_action :event_filter, only: :show layout :determine_layout @@ -133,7 +133,7 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar) + params.require(:group).permit(:name, :description, :path, :avatar, :public) end def load_events diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index d3597ef090..b5f3176461 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -6,33 +6,34 @@ class GroupsFinder private def all_groups(current_user) - if current_user - if current_user.authorized_groups.any? - # User has access to groups - # - # Return only: - # groups with public projects - # groups with internal projects - # groups with joined projects - # - group_ids = Project.public_and_internal_only.pluck(:namespace_id) + - current_user.authorized_groups.pluck(:id) - Group.where(id: group_ids) - else - # User has no group membership - # - # Return only: - # groups with public projects - # groups with internal projects - # - Group.where(id: Project.public_and_internal_only.pluck(:namespace_id)) - end - else - # Not authenticated - # - # Return only: - # groups with public projects - Group.where(id: Project.public_only.pluck(:namespace_id)) - end + group_ids = if current_user + if current_user.authorized_groups.any? + # User has access to groups + # + # Return only: + # groups with public projects + # groups with internal projects + # groups with joined projects + # + Project.public_and_internal_only.pluck(:namespace_id) + + current_user.authorized_groups.pluck(:id) + else + # User has no group membership + # + # Return only: + # groups with public projects + # groups with internal projects + # + Project.public_and_internal_only.pluck(:namespace_id) + end + else + # Not authenticated + # + # Return only: + # groups with public projects + Project.public_only.pluck(:namespace_id) + end + + Group.where("public IS TRUE OR id IN(?)", group_ids) end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c31a556ff7..a6ee688024 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -70,7 +70,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.search(term).limit(limit).map do |group| + GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group| { label: "group: #{search_result_sanitize(group.name)}", url: group_path(group) diff --git a/app/models/group.rb b/app/models/group.rb index 465c22d23a..34904af3b5 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -120,7 +120,7 @@ class Group < Namespace end def public_profile? - projects.public_only.any? + self.public || projects.public_only.any? end def post_create_hook diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index ae8fc9f85f..57308a661c 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -25,6 +25,15 @@ %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-sm remove-avatar" + .form-group + %hr + = f.label :public, class: 'control-label' do + Public + .col-sm-10 + .checkbox + = f.check_box :public + %span.descr Make this group public (even if there is no any public project inside this group) + .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb new file mode 100644 index 0000000000..635346300c --- /dev/null +++ b/db/migrate/20151103001141_add_public_to_group.rb @@ -0,0 +1,5 @@ +class AddPublicToGroup < ActiveRecord::Migration + def change + add_column :namespaces, :public, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 73fc83c3d6..17d445a8ba 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: 20151026182941) do +ActiveRecord::Schema.define(version: 20151103001141) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -501,14 +501,15 @@ ActiveRecord::Schema.define(version: 20151026182941) do add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree create_table "namespaces", force: true do |t| - t.string "name", null: false - t.string "path", null: false + t.string "name", null: false + t.string "path", null: false t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" t.string "type" - t.string "description", default: "", null: false + t.string "description", default: "", null: false t.string "avatar" + t.boolean "public", default: false end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree diff --git a/spec/finders/group_finder_spec.rb b/spec/finders/group_finder_spec.rb new file mode 100644 index 0000000000..78dc027837 --- /dev/null +++ b/spec/finders/group_finder_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe GroupsFinder do + let(:user) { create :user } + let!(:group) { create :group } + let!(:public_group) { create :group, public: true } + + describe :execute do + it 'finds public group' do + groups = GroupsFinder.new.execute(user) + expect(groups.size).to eq(1) + expect(groups.first).to eq(public_group) + end + end +end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index b327f4f911..ebe9c29d91 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -42,6 +42,11 @@ describe SearchHelper do expect(search_autocomplete_opts(project.name).size).to eq(1) end + it "includes the public group" do + group = create(:group, public: true) + expect(search_autocomplete_opts(group.name).size).to eq(1) + end + context "with a current project" do before { @project = create(:project) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 80638fc8db..0f23e81ace 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -84,4 +84,23 @@ describe Group do expect(group.avatar_type).to eq(["only images allowed"]) end end + + describe "public_profile?" do + it "returns true for public group" do + group = create(:group, public: true) + expect(group.public_profile?).to be_truthy + end + + it "returns true for non-public group with public project" do + group = create(:group) + create(:project, :public, group: group) + expect(group.public_profile?).to be_truthy + end + + it "returns false for non-public group with no public projects" do + group = create(:group) + create(:project, group: group) + expect(group.public_profile?).to be_falsy + end + end end From 850bb21b12b21fe0cf943278bc8cadad85d48dc5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 13:49:34 +0100 Subject: [PATCH 378/443] Create show page for tag and render release notes there and on index page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/callout.scss | 6 +++--- app/assets/stylesheets/pages/commits.scss | 7 +++++++ .../projects/releases_controller.rb | 6 +----- app/controllers/projects/tags_controller.rb | 7 +++++++ app/views/projects/branches/_commit.html.haml | 4 ++-- app/views/projects/releases/edit.html.haml | 3 +-- app/views/projects/tags/_tag.html.haml | 11 ++++++++++- .../{releases => tags}/show.html.haml | 19 ++++++++++++++----- config/routes.rb | 4 ++-- 9 files changed, 47 insertions(+), 20 deletions(-) rename app/views/projects/{releases => tags}/show.html.haml (68%) diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index f1699d21c9..f3ce4e3c21 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -9,9 +9,9 @@ .bs-callout { margin: 20px 0; padding: 20px; - border-left: 3px solid #eee; - color: #666; - background: #f9f9f9; + border-left: 3px solid $border-color; + color: $text-color; + background: $background-color; } .bs-callout h4 { margin-top: 0; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index e485487bcf..c9dfcff629 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -115,3 +115,10 @@ li.commit { } } } + +.branch-commit { + color: $gl-gray; + .commit-id, .commit-row-message { + color: $gl-gray; + } +} diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 7d1a011cc0..f69a4bc729 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -6,10 +6,6 @@ class Projects::ReleasesController < Projects::ApplicationController before_action :tag before_action :release - def show - @commit = @repository.commit(@tag.target) - end - def edit end @@ -18,7 +14,7 @@ class Projects::ReleasesController < Projects::ApplicationController release.update_attributes(description: description) release.save - redirect_to namespace_project_tag_release_path(@project.namespace, @project, @tag.name) + redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) end private diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index f565fbbbbc..dfc8dbe01c 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -8,6 +8,13 @@ class Projects::TagsController < Projects::ApplicationController def index sorted = VersionSorter.rsort(@repository.tag_names) @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) + @releases = project.releases.where(tag: @tags) + end + + def show + @tag = @repository.find_tag(params[:id]) + @release = @project.releases.find_or_initialize_by(tag: @tag.name) + @commit = @repository.commit(@tag.target) end def create diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 68326e65d8..22d77dda93 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,5 +1,5 @@ -.branch-commit.light - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" +.branch-commit + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id" · %span.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 612e469622..fb841b77a2 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -13,6 +13,5 @@ .error-alert .prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' - - if @release.persisted? - = link_to "Cancel", namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 526dd3f580..5d203903f8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,13 +1,17 @@ - commit = @repository.commit(tag.target) +- release = @releases.find { |release| release.tag == tag.name } %li %div - = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do %strong %i.fa.fa-tag = tag.name - if tag.message.present?   = strip_gpg_signature(tag.message) + - if release + %span.label.label-success release + .controls = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do = icon("pencil") @@ -22,3 +26,8 @@ - else %p Cant find HEAD commit for this tag + - if release && release.description.present? + .description.prepend-top-default + .wiki + = preserve do + = markdown release.description diff --git a/app/views/projects/releases/show.html.haml b/app/views/projects/tags/show.html.haml similarity index 68% rename from app/views/projects/releases/show.html.haml rename to app/views/projects/tags/show.html.haml index 606510f132..d2f23b96a8 100644 --- a/app/views/projects/releases/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,4 +1,4 @@ -- page_title @release.tag, "Releases" +- page_title @tag.name, "Tags" = render "projects/commits/header_title" = render "projects/commits/head" @@ -6,7 +6,14 @@ .pull-right = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn' do = icon("pencil") - .oneline Release notes for #{@tag.name} + - if @tag.message.present? + .title + %strong= @tag.name + = strip_gpg_signature(@tag.message) + - else + .oneline + .title + %strong= @tag.name .append-bottom-default.prepend-top-default - if @release.description.present? @@ -24,12 +31,14 @@ Commits - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: @tag.name, btn_class: 'btn-grouped' - -#- if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + - if can?(current_user, :admin_project, @project) + .pull-right + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + %i.fa.fa-trash-o .gray-content-block.second-block - if @commit = render 'projects/commits/commit', commit: @commit, project: @project - else Cant find HEAD commit for this tag + diff --git a/config/routes.rb b/config/routes.rb index 5836da47eb..c7aa95118e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -569,8 +569,8 @@ Gitlab::Application.routes.draw do end resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do - resource :release + resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do + resource :release, only: [:edit, :update] end resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } From 312cf11b61e6bbee8283dfb267516e6b42454431 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 14:03:48 +0100 Subject: [PATCH 379/443] Add release description to new tag form Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/dispatcher.js.coffee | 3 +++ app/controllers/projects/tags_controller.rb | 7 +++++++ app/views/projects/tags/new.html.haml | 19 ++++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 030826be74..951173af5d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -39,6 +39,9 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new DropzoneInput($('.merge-request-form')) new IssuableForm($('.merge-request-form')) + when 'projects:tags:new' + new ZenMode() + new DropzoneInput($('.tag-form')) when 'projects:releases:edit' new ZenMode() new DropzoneInput($('.release-form')) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index dfc8dbe01c..c4a3e3dca9 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -23,6 +23,13 @@ class Projects::TagsController < Projects::ApplicationController if result[:status] == :success @tag = result[:tag] + + if params[:release_description] + release = @project.releases.find_or_initialize_by(tag: @tag.name) + release.update_attributes(description: params[:release_description]) + release.save + end + redirect_to namespace_project_tags_path(@project.namespace, @project) else @error = result[:message] diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 9f5c1be125..9b224ff89b 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -8,7 +8,7 @@ %h3.page-title %i.fa.fa-code-fork New tag -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal tag-form" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' .col-sm-10 @@ -23,6 +23,23 @@ .col-sm-10 = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' .light (Optional) Entering a message will create an annotated tag. + %hr + .form-group + = label_tag :release_description, 'Release description', class: 'control-label' + .col-sm-10 + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + .zennable + %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") + .zen-backdrop + = text_area_tag :release_description, nil, class: 'js-gfm-input markdown-area description js-quick-submit form-control', placeholder: '' + %a.zen-enter-link(tabindex="-1" href="#") + = icon('expand') + Edit in fullscreen + %a.zen-leave-link(href="#") + = icon('compress') + + = render 'projects/notes/hints' + .help-block You can add release description to your tag. It will be stored in GitLab database and displayed on tags page .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' From 26677fbe213069a3820f9f20d528bd560d447bea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 14:07:55 +0100 Subject: [PATCH 380/443] After tag is created - redirect to tag page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/tags_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index c4a3e3dca9..055f328677 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -30,7 +30,7 @@ class Projects::TagsController < Projects::ApplicationController release.save end - redirect_to namespace_project_tags_path(@project.namespace, @project) + redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) else @error = result[:message] render action: 'new' From ba68facf8d744f6de49b40a2e9923e6555c92cd7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 3 Nov 2015 11:44:07 +0100 Subject: [PATCH 381/443] CI details cleanup - Add page titles to CI settings. - Fix CI admin navigation. - Remove duplicated scope. - Use monospace font for commit sha. - Add page title and header title to build page. - Proper authorization for cancel/retry builds. - Use gitlab pagination theme for builds and group members. - Don't paginate builds widget on build page. - Add badges to commit page Changes/Builds tabs. - Add "Builds" to commit Builds tab page title. - Add and use Ci::Build#retryable? method. - Add CI::Build#retried? method. - Allow all failed commit builds to be retried. - Proper authorization for cancel/retry all builds. - Remove unused param. - Use time_ago_with_tooltip where appropriate. - Tweak builds index text - Remove duplication between builds/build and commit_statuses/commit_status. - Use POST rather than GET for canceling and retrying builds. - Remove redundant URL helpers. - Add build ID to build page. - Link branch name on build page. - Move commit/:sha/ci to commit/:sha/builds. --- app/controllers/projects/builds_controller.rb | 10 ++-- app/controllers/projects/commit_controller.rb | 38 ++++++++----- app/helpers/builds_helper.rb | 13 ----- app/helpers/ci/gitlab_helper.rb | 19 ------- app/helpers/ci_status_helper.rb | 2 +- app/models/ci/build.rb | 10 +++- app/models/commit_status.rb | 4 +- .../project_services/ci/hip_chat_message.rb | 2 +- .../project_services/ci/slack_message.rb | 2 +- .../project_services/gitlab_ci_service.rb | 2 +- .../ci/notify/build_fail_email.html.haml | 4 +- .../ci/notify/build_success_email.html.haml | 4 +- app/views/dashboard/groups/index.html.haml | 2 +- app/views/layouts/ci/_nav_admin.html.haml | 18 ++++--- app/views/projects/_last_commit.html.haml | 2 +- app/views/projects/builds/_build.html.haml | 53 ------------------- .../projects/builds/_header_title.html.haml | 1 + app/views/projects/builds/index.html.haml | 12 ++--- app/views/projects/builds/show.html.haml | 39 +++++++------- app/views/projects/ci_services/edit.html.haml | 1 + .../projects/ci_services/index.html.haml | 1 + app/views/projects/ci_settings/edit.html.haml | 1 + .../projects/ci_web_hooks/index.html.haml | 1 + app/views/projects/commit/_ci_menu.html.haml | 6 ++- .../projects/commit/_commit_box.html.haml | 4 +- .../commit/{ci.html.haml => builds.html.haml} | 9 ++-- .../commit_statuses/_commit_status.html.haml | 31 ++++++++--- app/views/projects/runners/edit.html.haml | 2 + app/views/projects/runners/index.html.haml | 1 + app/views/projects/runners/show.html.haml | 21 ++++---- app/views/projects/triggers/index.html.haml | 1 + app/views/projects/variables/show.html.haml | 1 + config/routes.rb | 9 ++-- spec/features/builds_spec.rb | 14 +++-- spec/features/commits_spec.rb | 2 +- .../gitlab_ci_service_spec.rb | 2 +- 36 files changed, 160 insertions(+), 184 deletions(-) delete mode 100644 app/helpers/builds_helper.rb delete mode 100644 app/views/projects/builds/_build.html.haml create mode 100644 app/views/projects/builds/_header_title.html.haml rename app/views/projects/commit/{ci.html.haml => builds.html.haml} (77%) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 7d72e0b951..953f30e7c0 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -30,7 +30,7 @@ class Projects::BuildsController < Projects::ApplicationController def show @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC') - @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) + @builds = @builds.where("id not in (?)", @build.id) @commit = @build.commit respond_to do |format| @@ -42,17 +42,13 @@ class Projects::BuildsController < Projects::ApplicationController end def retry - if @build.commands.blank? + unless @build.retryable? return page_404 end build = Ci::Build.retry(@build) - if params[:return_to] - redirect_to URI.parse(params[:return_to]).path - else - redirect_to build_path(build) - end + redirect_to build_path(build) end def status diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 878c3a66e7..deefdd7666 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -7,14 +7,14 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_download_code!, except: [:cancel_builds] before_action :authorize_manage_builds!, only: [:cancel_builds] before_action :commit + before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds] + before_action :define_show_vars, only: [:show, :builds] def show return git_not_found! unless @commit @line_notes = commit.notes.inline - @diffs = @commit.diffs @note = @project.build_commit_note(commit) - @notes_count = commit.notes.count @notes = commit.notes.not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @@ -23,8 +23,6 @@ class Projects::CommitController < Projects::ApplicationController commit_id: @commit.id } - @ci_commit = project.ci_commit(commit.sha) - respond_to do |format| format.html format.diff { render text: @commit.to_diff } @@ -32,20 +30,25 @@ class Projects::CommitController < Projects::ApplicationController end end - def ci - @ci_commit = @project.ci_commit(@commit.sha) - @builds = @ci_commit.builds if @ci_commit - @notes_count = @commit.notes.count + def builds @ci_project = @project.gitlab_ci_project end def cancel_builds - @ci_commit = @project.ci_commit(@commit.sha) - @ci_commit.builds.running_or_pending.each(&:cancel) + ci_commit.builds.running_or_pending.each(&:cancel) - redirect_to ci_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) end + def retry_builds + ci_commit.builds.latest.failed.each do |build| + if build.retryable? + Ci::Build.retry(build) + end + end + + redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) + end def branches @branches = @project.repository.branch_names_contains(commit.id) @@ -53,11 +56,22 @@ class Projects::CommitController < Projects::ApplicationController render layout: false end + private + def commit @commit ||= @project.commit(params[:id]) end - private + def ci_commit + @ci_commit ||= project.ci_commit(commit.sha) + end + + def define_show_vars + @diffs = commit.diffs + @notes_count = commit.notes.count + + @builds = ci_commit.builds if ci_commit + end def authorize_manage_builds! unless can?(current_user, :manage_builds, project) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb deleted file mode 100644 index 1b5a2c31d7..0000000000 --- a/app/helpers/builds_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module BuildsHelper - def build_ref_link build - gitlab_ref_link build.project, build.ref - end - - def build_commit_link build - gitlab_commit_link build.project, build.short_sha - end - - def build_url(build) - namespace_project_build_path(build.gl_project, build.project, build) - end -end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb index baddbc806f..e34c8be1df 100644 --- a/app/helpers/ci/gitlab_helper.rb +++ b/app/helpers/ci/gitlab_helper.rb @@ -4,25 +4,6 @@ module Ci { :"data-no-turbolink" => "data-no-turbolink" } end - def gitlab_ref_link project, ref - gitlab_url = project.gitlab_url.dup - gitlab_url << "/commits/#{ref}" - link_to ref, gitlab_url, no_turbolink - end - - def gitlab_compare_link project, before, after - gitlab_url = project.gitlab_url.dup - gitlab_url << "/compare/#{before}...#{after}" - - link_to "#{before}...#{after}", gitlab_url, no_turbolink - end - - def gitlab_commit_link project, sha - gitlab_url = project.gitlab_url.dup - gitlab_url << "/commit/#{sha}" - link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink - end - def yaml_web_editor_link(project) commits = project.commits diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index ed88df5dd8..0ecf77bb45 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,7 +1,7 @@ module CiStatusHelper def ci_status_path(ci_commit) project = ci_commit.gl_project - ci_namespace_project_commit_path(project.namespace, project, ci_commit.sha) + builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end def ci_status_icon(ci_commit) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b19e2ac136..7f185ae7cc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -106,6 +106,14 @@ module Ci failed? && allow_failure? end + def retryable? + commands.present? + end + + def retried? + !self.commit.latest_builds_for_ref(self.ref).include?(self) + end + def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? html || '' @@ -222,7 +230,7 @@ module Ci end def retry_url - if commands.present? + if retryable? Gitlab::Application.routes.url_helpers. retry_namespace_project_build_path(gl_project.namespace, gl_project, self) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0b73ab6d2e..7d54d83974 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -15,8 +15,8 @@ class CommitStatus < ActiveRecord::Base scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } scope :failed, -> { where(status: 'failed') } - scope :running_or_pending, -> { where(status:[:running, :pending]) } - scope :finished, -> { where(status:[:success, :failed, :canceled]) } + scope :running_or_pending, -> { where(status: [:running, :pending]) } + scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb index cbf325cc52..d89466b689 100644 --- a/app/models/project_services/ci/hip_chat_message.rb +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -11,7 +11,7 @@ module Ci def to_s lines = Array.new lines.push("#{project.name} - ") - lines.push("Commit ##{commit.id}
    ") + lines.push("Commit ##{commit.id}
    ") lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}
    ") lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") lines.join('') diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb index dc050a3fc5..1a6ff8e34c 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -45,7 +45,7 @@ module Ci def attachment_message out = "<#{ci_project_url(project)}|#{project_name}>: " - out << "Commit <#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> " + out << "Commit <#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> " out << "(<#{commit_sha_link}|#{commit.short_sha}>) " out << "of <#{commit_ref_link}|#{commit.ref}> " out << "by #{commit.git_author_name} " if commit.git_author_name diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 4dcd16ede3..095d04e0df 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -71,7 +71,7 @@ class GitlabCiService < CiService def build_page(sha, ref) if project.gitlab_ci_project.present? - ci_namespace_project_commit_url(project.namespace, project, sha) + builds_namespace_project_commit_url(project.namespace, project, sha) end end diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml index 69689a7502..cefb75040e 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -7,7 +7,7 @@ = @project.name %p - Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_path(@build.gl_project.namespace, @build.gl_project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p @@ -16,4 +16,4 @@ Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml index 4e3015a356..617b88f734 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -8,7 +8,7 @@ = @project.name %p - Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_path(@build.gl_project.namespace, @build.gl_project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p @@ -17,4 +17,4 @@ Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index c249f5cace..f3f3f58111 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -16,4 +16,4 @@ - group = group_member.group = render 'shared/groups/group', group: group, group_member: group_member -= paginate @group_members += paginate @group_members, theme: 'gitlab' diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml index af2545a22d..dcda04a463 100644 --- a/app/views/layouts/ci/_nav_admin.html.haml +++ b/app/views/layouts/ci/_nav_admin.html.haml @@ -9,23 +9,25 @@ = nav_link path: 'projects#index' do = link_to ci_admin_projects_path do = icon('list-alt fw') - Projects + %span + Projects = nav_link path: 'events#index' do = link_to ci_admin_events_path do = icon('book fw') - Events + %span + Events = nav_link path: ['runners#index', 'runners#show'] do = link_to ci_admin_runners_path do = icon('cog fw') - Runners - %small.pull-right - = Ci::Runner.count(:all) + %span + Runners + %span.count= Ci::Runner.count(:all) = nav_link path: 'builds#index' do = link_to ci_admin_builds_path do = icon('link fw') - Builds - %small.pull-right - = Ci::Build.count(:all) + %span + Builds + %span.count= Ci::Build.count(:all) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to ci_admin_application_settings_path do = icon('cogs fw') diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index d7b20bfc6b..7e1ee2b7fc 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -6,7 +6,7 @@ = ci_commit.status = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" · #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml deleted file mode 100644 index 4ce4ed63b4..0000000000 --- a/app/views/projects/builds/_build.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -%tr.build - %td.status - = ci_status_with_icon(build.status) - - %td.commit_status-link - - if build.target_url - = link_to build.target_url do - %strong Build ##{build.id} - - else - %strong Build ##{build.id} - - - if build.show_warning? - %i.fa.fa-warning.text-warning - - %td - = link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha) - - %td - = link_to build.ref, namespace_project_commits_path(@project.namespace, @project, build.ref) - - %td - - if build.runner - = runner_link(build.runner) - - else - .light none - - %td - = build.name - - .pull-right - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.trigger_request - %span.label.label-info triggered - - if build.allow_failure - %span.label.label-danger allowed to fail - - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp - - if build.finished_at - %span #{time_ago_in_words build.finished_at} ago - - %td - .pull-right - - if current_user && can?(current_user, :manage_builds, @project) - - if build.cancel_url - = link_to build.cancel_url, title: 'Cancel' do - %i.fa.fa-remove.cred diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml new file mode 100644 index 0000000000..082dab1f5b --- /dev/null +++ b/app/views/projects/builds/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Builds", project_builds_path(@project)) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index e08556673e..dab7164153 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,12 +1,12 @@ - page_title "Builds" -- header_title project_title(@project, "Builds", project_builds_path(@project)) += render "header_title" .project-issuable-filter .controls - if @ci_project && current_user && can?(current_user, :manage_builds, @project) .pull-left.hidden-xs - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' + = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post %ul.center-top-menu %li{class: ('active' if @scope.nil?)} @@ -25,7 +25,7 @@ %span.badge.js-totalbuilds-count= @all_builds.count(:id) .gray-content-block - List of #{@scope || 'running'} builds from this project + #{(@scope || 'running').capitalize} builds from this project %ul.content-list - if @builds.blank? @@ -40,14 +40,14 @@ %th Build ID %th Commit %th Ref - %th Runner + %th Stage %th Name %th Duration %th Finished at %th - @builds.each do |build| - = render 'projects/builds/build', build: build + = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true - = paginate @builds + = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index e3d8d73491..3374d5432a 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,10 +1,13 @@ +- page_title "#{@build.name} (#{@build.id})", "Builds" += render "header_title" + .build-page .gray-content-block - Build for commit + Build ##{@build.id} for commit %strong.monospace = link_to @build.commit.short_sha, ci_status_path(@build.commit) from - %code #{@build.ref} + = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) #up-build-trace - if @commit.matrix_for_ref?(@build.ref) @@ -20,7 +23,7 @@ = build.id - - unless @commit.latest_builds_for_ref(@build.ref).include?(@build) + - if @build.retried? %li.active %a Build ##{@build.id} @@ -37,7 +40,7 @@ %i.fa.fa-time #{duration_in_words(@build.finished_at, @build.started_at)} .pull-right - = @build.updated_at.stamp('19:00 Aug 27') + #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at} - if @build.show_warning? - unless @build.any_runners_online? @@ -87,13 +90,13 @@ .build-widget %h4.title - Build + Build ##{@build.id} - if current_user && can?(current_user, :manage_builds, @project) .pull-right - - if @build.active? - = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger' - - elsif @build.commands.present? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post + - if @build.cancel_url + = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post + - elsif @build.retry_url + = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post - if @build.duration %p @@ -101,15 +104,15 @@ #{duration_in_words(@build.finished_at, @build.started_at)} %p %span.attr-name Created: - #{time_ago_in_words(@build.created_at)} ago + #{time_ago_with_tooltip(@build.created_at)} - if @build.finished_at %p %span.attr-name Finished: - #{time_ago_in_words(@build.finished_at)} ago + #{time_ago_with_tooltip(@build.finished_at)} %p %span.attr-name Runner: - if @build.runner && current_user && current_user.admin - \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)} + = link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id) - elsif @build.runner \##{@build.runner.id} @@ -134,10 +137,11 @@ %h4.title Commit .pull-right - %small #{build_commit_link @build} + %small + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %p %span.attr-name Branch: - #{build_ref_link @build} + = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) %p %span.attr-name Author: #{@build.commit.git_author_name} @@ -155,7 +159,9 @@ - if @builds.present? .build-widget - %h4.title #{pluralize(@builds.count(:id), "other build")} for #{@build.short_sha}: + %h4.title #{pluralize(@builds.count(:id), "other build")} for + = succeed ":" do + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %table.table.builds - @builds.each_with_index do |build, i| %tr.build @@ -171,8 +177,5 @@ %td.status= build.status - = paginate @builds - - :javascript new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") diff --git a/app/views/projects/ci_services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml index bcc5832792..dacb6b4f6f 100644 --- a/app/views/projects/ci_services/edit.html.haml +++ b/app/views/projects/ci_services/edit.html.haml @@ -1 +1,2 @@ +- page_title @service.title, "CI Services" = render 'form' diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml index c164b2d4bc..3f26c7851d 100644 --- a/app/views/projects/ci_services/index.html.haml +++ b/app/views/projects/ci_services/index.html.haml @@ -1,3 +1,4 @@ +- page_title "CI Services" %h3.page-title Project services %p.light Project services allow you to integrate GitLab CI with other applications diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml index eedf484bf0..665556f5c2 100644 --- a/app/views/projects/ci_settings/edit.html.haml +++ b/app/views/projects/ci_settings/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "CI Settings" - if @ci_project.generated_yaml_config %p.alert.alert-danger CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)} diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml index 369086b39e..2998fb08ff 100644 --- a/app/views/projects/ci_web_hooks/index.html.haml +++ b/app/views/projects/ci_web_hooks/index.html.haml @@ -1,3 +1,4 @@ +- page_title "CI Web Hooks" %h3.page-title CI Web hooks diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index a634ae5dfd..c73ba74f5e 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -2,6 +2,8 @@ = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes - = nav_link(path: 'commit#ci') do - = link_to ci_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + %span.badge= @diffs.count + = nav_link(path: 'commit#builds') do + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds + %span.badge= @builds.count(:id) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index fbf0a9ec0c..a6458b8486 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -19,7 +19,7 @@ %p %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit) + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" .commit-info-row %span.light Authored by %strong @@ -36,7 +36,7 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent) + = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" - if @ci_commit .pull-right diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/builds.html.haml similarity index 77% rename from app/views/projects/commit/ci.html.haml rename to app/views/projects/commit/builds.html.haml index 43033cad24..f7a89232e0 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" = render "commit_box" = render "ci_menu" @@ -26,8 +26,11 @@   - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + - if @ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-primary', method: :post + - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel all", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger' + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger', method: :post .table-holder %table.table.builds @@ -45,7 +48,7 @@ %th - @ci_commit.refs.each do |ref| = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true } + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } - if @ci_commit.retried.any? .gray-content-block.second-block diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 637154f56a..c255559b88 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -12,14 +12,30 @@ - if commit_status.show_warning? %i.fa.fa-warning.text-warning - %td - = commit_status.ref + - if defined?(commit_sha) && commit_sha + %td + = link_to commit_status.short_sha, namespace_project_commit_path(@project.namespace, @project, commit_status.sha), class: "monospace" %td - = commit_status.stage + - if commit_status.ref + = link_to commit_status.ref, namespace_project_commits_path(@project.namespace, @project, commit_status.ref) + - else + .light none + + - if defined?(runner) && runner + %td + - if commit_status.try(:runner) + = runner_link(commit_status.runner) + - else + .light none + + - if defined?(stage) && stage + %td + = commit_status.stage %td = commit_status.name + .pull-right - if commit_status.tags.any? - commit_status.tags.each do |tag| @@ -36,7 +52,7 @@ %td.timestamp - if commit_status.finished_at - %span #{time_ago_in_words commit_status.finished_at} ago + %span #{time_ago_with_tooltip(commit_status.finished_at)} - if defined?(coverage) && coverage %td.coverage @@ -46,9 +62,10 @@ %td .pull-right - if current_user && can?(current_user, :manage_builds, commit_status.gl_project) - - if commit_status.cancel_url - = link_to commit_status.cancel_url, title: 'Cancel' do - %i.fa.fa-remove.cred + - if commit_status.active? + - if commit_status.cancel_url + = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do + %i.fa.fa-remove.cred - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url = link_to commit_status.retry_url, method: :post, title: 'Retry' do %i.fa.fa-repeat diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index 66851d3831..dde9e448cb 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,3 +1,5 @@ +- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" + %h4 Runner ##{@runner.id} %hr = form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 529fb9c296..315afe4a76 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Runners" .light %p A 'runner' is a process which runs a build. diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index c255cd51bd..5bf4c09ca2 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -1,13 +1,14 @@ -= content_for :title do - %h3.project-title - Runner ##{@runner.id} - .pull-right - - if @runner.shared? - %span.runner-state.runner-state-shared - Shared - - else - %span.runner-state.runner-state-specific - Specific +- page_title "#{@runner.description} ##{@runner.id}", "Runners" + +%h3.page-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific .table-holder %table.table diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 18a37302c3..b3ad79a200 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Triggers" %h3.page-title Triggers diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index 29416a94ff..e052da1ac4 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Variables" %h3.page-title Secret Variables diff --git a/config/routes.rb b/config/routes.rb index 0458f538eb..990a00e3d0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -472,8 +472,9 @@ Gitlab::Application.routes.draw do resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do member do get :branches - get :ci - get :cancel_builds + get :builds + post :cancel_builds + post :retry_builds end end @@ -588,12 +589,12 @@ Gitlab::Application.routes.draw do resources :builds, only: [:index, :show] do collection do - get :cancel_all + post :cancel_all end member do - get :cancel get :status + post :cancel post :retry end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 154857e77f..158e85e598 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -47,10 +47,11 @@ describe "Builds" do end end - describe "GET /:project/builds/:id/cancel_all" do + describe "POST /:project/builds/:id/cancel_all" do before do @build.run! - visit cancel_all_namespace_project_builds_path(@gl_project.namespace, @gl_project) + visit namespace_project_builds_path(@gl_project.namespace, @gl_project) + click_link "Cancel all" end it { expect(page).to have_content 'No builds to show' } @@ -67,10 +68,11 @@ describe "Builds" do it { expect(page).to have_content @commit.git_author_name } end - describe "GET /:project/builds/:id/cancel" do + describe "POST /:project/builds/:id/cancel" do before do @build.run! - visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link "Cancel" end it { expect(page).to have_content 'canceled' } @@ -79,7 +81,9 @@ describe "Builds" do describe "POST /:project/builds/:id/retry" do before do - visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + @build.run! + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link "Cancel" click_link 'Retry' end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 1adc2cdf70..340924fafe 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -32,7 +32,7 @@ describe "Commits" do describe "Cancel all builds" do it "cancels commit" do visit ci_status_path(@commit) - click_on "Cancel all" + click_on "Cancel running" expect(page).to have_content "canceled" end end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 842089ebe0..b9006b693b 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -39,7 +39,7 @@ describe GitlabCiService do end describe :build_page do - it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/ci")} + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/builds")} end describe "execute" do From 2becd53015c5b283d8c5bfc6b8702b22416e55d2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 3 Nov 2015 14:24:00 +0100 Subject: [PATCH 382/443] Add missing stage to builds view --- app/views/projects/commit/builds.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index f7a89232e0..00cf9c7610 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -69,4 +69,4 @@ %th Coverage %th = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?) } + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } From aecc4376e20b3ad23662f6181a776ac5747baa06 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 5 Nov 2015 16:41:03 +0200 Subject: [PATCH 383/443] make migrations reversible --- db/migrate/20151019111551_fix_build_tags.rb | 6 +++++- db/migrate/20151019111703_fail_build_without_names.rb | 5 ++++- db/migrate/20151023112551_fail_build_with_empty_name.rb | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb index 84b142183f..299a24b0a7 100644 --- a/db/migrate/20151019111551_fix_build_tags.rb +++ b/db/migrate/20151019111551_fix_build_tags.rb @@ -1,5 +1,9 @@ class FixBuildTags < ActiveRecord::Migration - def change + def up execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'") end + + def down + execute("UPDATE taggings SET taggable_type='Ci::Build' WHERE taggable_type='CommitStatus'") + end end diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb index 546b03d812..dcdb5d1b25 100644 --- a/db/migrate/20151019111703_fail_build_without_names.rb +++ b/db/migrate/20151019111703_fail_build_without_names.rb @@ -1,5 +1,8 @@ class FailBuildWithoutNames < ActiveRecord::Migration - def change + def up execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'") end + + def down + end end diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb index f069bc60ac..41c0f0649c 100644 --- a/db/migrate/20151023112551_fail_build_with_empty_name.rb +++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb @@ -1,5 +1,8 @@ class FailBuildWithEmptyName < ActiveRecord::Migration - def change + def up execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'") end + + def down + end end From 6fcd5127a316ce3abf1e5c5dde5b1901e8c7941f Mon Sep 17 00:00:00 2001 From: Thirumal S Date: Thu, 5 Nov 2015 20:21:46 +0530 Subject: [PATCH 384/443] defined class prepend-top-10 used for alignment issue --- app/assets/stylesheets/pages/projects.scss | 4 ---- app/views/projects/hooks/index.html.haml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2e7ad1173a..d3b1004002 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -552,8 +552,4 @@ pre.light-well { z-index: 100; position: relative; } -} - -.form-control-padding-top { - padding-top: 10px; } \ No newline at end of file diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 65e00b38ad..3702aeaecb 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -19,7 +19,7 @@ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' .form-group = f.label :url, "Trigger", class: 'control-label' - .col-sm-10.form-control-padding-top + .col-sm-10.prepend-top-10 %div = f.check_box :push_events, class: 'pull-left' .prepend-left-20 From b18671a1b2c565a87663544441000063f6b83c8e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 3 Nov 2015 14:45:41 +0100 Subject: [PATCH 385/443] Enable shared runners for all new projects --- CHANGELOG | 1 + app/controllers/admin/application_settings_controller.rb | 1 + app/models/application_setting.rb | 3 ++- app/models/project.rb | 5 ++++- app/views/admin/application_settings/_form.html.haml | 9 +++++++++ config/initializers/1_settings.rb | 9 +++++---- db/migrate/20151103133339_add_shared_runners_setting.rb | 5 +++++ db/schema.rb | 3 ++- lib/gitlab/current_settings.rb | 3 ++- 9 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20151103133339_add_shared_runners_setting.rb diff --git a/CHANGELOG b/CHANGELOG index 3a75f50e9a..2201221116 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.2.0 (unreleased) - Use git follow flag for commits page when retrieve history for file or directory - Show merge request CI status on merge requests index page - Extend yml syntax for only and except to support specifying repository path + - Enable shared runners to all new projects - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page - Use issue editor as cross reference comment author when issue is edited with a new mention. diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 039f18f23e..3d9c59050f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -57,6 +57,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :admin_notification_email, :user_oauth_applications, + :shared_runners_enabled, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 05430c2ee1..266045f7af 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -87,7 +87,8 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], ) end diff --git a/app/models/project.rb b/app/models/project.rb index 74b89aad49..57db7f9f0a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,7 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel + include Gitlab::CurrentSettings include Referable include Sortable include AfterCommitQueue @@ -775,7 +776,9 @@ class Project < ActiveRecord::Base end def ensure_gitlab_ci_project - gitlab_ci_project || create_gitlab_ci_project + gitlab_ci_project || create_gitlab_ci_project( + shared_runners_enabled: current_application_settings.shared_runners_enabled + ) end def enable_ci diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 7a78526e09..7253218c2e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -130,5 +130,14 @@ = f.text_area :help_page_text, class: 'form-control', rows: 4 .help-block Markdown enabled + %fieldset + %legend Continuous Integration + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :shared_runners_enabled do + = f.check_box :shared_runners_enabled + Enable shared runners for a new projects + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index c189c88bdc..64d73d7232 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -181,10 +181,11 @@ Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious' # CI # Settings['gitlab_ci'] ||= Settingslogic.new({}) -Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? -Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? -Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) -Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil? +Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? +Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) +Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) # # Reply by email diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb new file mode 100644 index 0000000000..4231dfd5c2 --- /dev/null +++ b/db/migrate/20151103133339_add_shared_runners_setting.rb @@ -0,0 +1,5 @@ +class AddSharedRunnersSetting < ActiveRecord::Migration + def up + add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 17d445a8ba..de896f2764 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: 20151103001141) do +ActiveRecord::Schema.define(version: 20151103133339) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -47,6 +47,7 @@ ActiveRecord::Schema.define(version: 20151103001141) do t.text "import_sources" t.text "help_page_text" t.string "admin_notification_email" + t.boolean "shared_runners_enabled", default: true, null: false end create_table "audit_events", force: true do |t| diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 0ea1b6a2f6..cd84afa31d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -23,7 +23,8 @@ module Gitlab restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], - import_sources: Settings.gitlab['import_sources'] + import_sources: Settings.gitlab['import_sources'], + shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], ) end From 900419c43c5a540cde22f5488675121b3ce05d31 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 17:08:47 +0100 Subject: [PATCH 386/443] Improve UI for tags page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/tags_controller.rb | 10 +++------- app/views/projects/tags/_download.html.haml | 17 ++++++++++++++++ app/views/projects/tags/_tag.html.haml | 8 +++----- app/views/projects/tags/destroy.js.haml | 3 --- app/views/projects/tags/show.html.haml | 22 ++++++++++----------- 5 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 app/views/projects/tags/_download.html.haml delete mode 100644 app/views/projects/tags/destroy.js.haml diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 055f328677..670f5d3067 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -39,13 +39,9 @@ class Projects::TagsController < Projects::ApplicationController def destroy DeleteTagService.new(project, current_user).execute(params[:id]) + release = project.releases.find_by(tag: params[:id]) + release.destroy if release - respond_to do |format| - format.html do - redirect_to namespace_project_tags_path(@project.namespace, - @project) - end - format.js - end + redirect_to namespace_project_tags_path(@project.namespace, @project) end end diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml new file mode 100644 index 0000000000..667057ef2d --- /dev/null +++ b/app/views/projects/tags/_download.html.haml @@ -0,0 +1,17 @@ +%span.btn-group.btn-grouped + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do + %i.fa.fa-download + %span source code + %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Archive Format + %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 5d203903f8..f3dc314a89 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -4,22 +4,20 @@ %div = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do %strong - %i.fa.fa-tag + = icon('tag') = tag.name - if tag.message.present?   = strip_gpg_signature(tag.message) - if release +   %span.label.label-success release .controls = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do = icon("pencil") - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' - - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + = render 'projects/tags/download', ref: tag.name, project: @project - if commit = render 'projects/branches/commit', commit: commit, project: @project diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml deleted file mode 100644 index ada6710f94..0000000000 --- a/app/views/projects/tags/destroy.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}") -- if @repository.tags.size == 0 - $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index d2f23b96a8..22697464fd 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -6,6 +6,16 @@ .pull-right = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn' do = icon("pencil") + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + = icon('files-o') + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + = icon('history') + - if can? current_user, :download_code, @project + = render 'projects/tags/download', ref: @tag.name, project: @project + - if can?(current_user, :admin_project, @project) + .pull-right + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'} do + %i.fa.fa-trash-o - if @tag.message.present? .title %strong= @tag.name @@ -24,18 +34,6 @@ - else This tag has no release notes yet. Press edit button to add one -.gray-content-block.middle-block.clearfix - = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do - Browse code - = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do - Commits - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @tag.name, btn_class: 'btn-grouped' - - if can?(current_user, :admin_project, @project) - .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o - .gray-content-block.second-block - if @commit = render 'projects/commits/commit', commit: @commit, project: @project From 3c0244d3d1533dab3276465a63a26e0937aef543 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Nov 2015 18:37:31 +0100 Subject: [PATCH 387/443] Retyle tag show page Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/tags/show.html.haml | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 22697464fd..f80ba5516a 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -16,14 +16,17 @@ .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'} do %i.fa.fa-trash-o - - if @tag.message.present? - .title - %strong= @tag.name - = strip_gpg_signature(@tag.message) + .title + %strong= @tag.name + - if @tag.message.present? + %span.light +   + = strip_gpg_signature(@tag.message) + - if @commit + = render 'projects/branches/commit', commit: @commit, project: @project - else - .oneline - .title - %strong= @tag.name + Cant find HEAD commit for this tag + .append-bottom-default.prepend-top-default - if @release.description.present? @@ -33,10 +36,3 @@ = markdown @release.description - else This tag has no release notes yet. Press edit button to add one - -.gray-content-block.second-block - - if @commit - = render 'projects/commits/commit', commit: @commit, project: @project - - else - Cant find HEAD commit for this tag - From c55b5c72e94344d138af7de198016c8ea54b80f8 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 5 Nov 2015 21:09:02 +0200 Subject: [PATCH 388/443] Remove duplicate documentation links --- doc/README.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/doc/README.md b/doc/README.md index a0ff856ebb..3cd98fab0d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -17,20 +17,20 @@ ## CI Documentation -+ [Quick Start](ci/quick_start/README.md) -+ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md) -+ [Configuring runner](ci/runners/README.md) -+ [Configuring deployment](ci/deployment/README.md) -+ [Using Docker Images](ci/docker/using_docker_images.md) -+ [Using Docker Build](ci/docker/using_docker_build.md) -+ [Using Variables](ci/variables/README.md) +- [Quick Start](ci/quick_start/README.md) +- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md) +- [Configuring runner](ci/runners/README.md) +- [Configuring deployment](ci/deployment/README.md) +- [Using Docker Images](ci/docker/using_docker_images.md) +- [Using Docker Build](ci/docker/using_docker_build.md) +- [Using Variables](ci/variables/README.md) ### CI Examples -+ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) -+ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md) -+ [Test Clojure applications](ci/examples/test-clojure-application.md) -+ Help your favorite programming language and GitLab by sending a merge request with a guide for that language. +- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) +- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md) +- [Test Clojure applications](ci/examples/test-clojure-application.md) +- Help your favorite programming language and GitLab by sending a merge request with a guide for that language. ## Administrator documentation @@ -49,11 +49,6 @@ - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. -### Administrator documentation - -+ [User permissions](permissions/permissions.md) -+ [API](api/README.md) - ## Contributor documentation - [Development](development/README.md) Explains the architecture and the guidelines for shell commands. From 5f696d0c22dd367fb4687b2d18f8db92e179bbcc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 5 Nov 2015 21:21:12 +0200 Subject: [PATCH 389/443] Add CI permissions and api links to README [ci skip] --- doc/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/README.md b/doc/README.md index 3cd98fab0d..0f6866475f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -24,6 +24,8 @@ - [Using Docker Images](ci/docker/using_docker_images.md) - [Using Docker Build](ci/docker/using_docker_build.md) - [Using Variables](ci/variables/README.md) +- [User permissions](ci/permissions/README.md) +- [API](ci/api/README.md) ### CI Examples From c468461579f0dedc0ad92609e6bb54624c2e2b04 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Thu, 5 Nov 2015 21:17:15 -0800 Subject: [PATCH 390/443] Only have one link in emails to make clicking it easier. --- app/views/layouts/notify.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 854cda57c3..f58b9bd6ba 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -40,9 +40,9 @@ Reply to this email directly or #{link_to "view it on GitLab", @target_url}. - else - #{link_to "View it on GitLab", @target_url} + #{link_to "View it on GitLab", @target_url}. %br - You're receiving this email because of your account on #{link_to Gitlab.config.gitlab.host, root_url}. + You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. If you'd like to receive fewer emails, you can adjust your notification settings. = email_action @target_url From 40a9b1ce00ed4e4f03431b731ca5a6716356d5a6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 5 Nov 2015 23:47:04 -0800 Subject: [PATCH 391/443] Fix bug where manually merged branches in a MR would end up with an empty diff Closes #3314 --- CHANGELOG | 1 + app/services/merge_requests/refresh_service.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 16055208db..3614d79036 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.2.0 (unreleased) + - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu) - Improved performance of finding users by one of their Email addresses - Improved performance of replacing references in comments diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index d68bc79ecc..e180edb4bf 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -7,17 +7,17 @@ module MergeRequests @branch_name = Gitlab::Git.ref_name(ref) find_new_commits + # Be sure to close outstanding MRs before reloading them to avoid generating an + # empty diff during a manual merge + close_merge_requests reload_merge_requests # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? comment_mr_branch_presence_changed - comment_mr_with_commits - else - comment_mr_with_commits - close_merge_requests end + comment_mr_with_commits execute_mr_web_hooks true From cea2afa85ed81abfa809a622ca3a548f770ad228 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 14:14:43 +0100 Subject: [PATCH 392/443] Add changelog item and remove release labels for tags list Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/projects/tags/_tag.html.haml | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0ec6030b13..a7a9e073ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.2.0 (unreleased) - Include commit logs in project search - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from + - Ability to add release notes (markdown text and attachments) to git tags v 8.1.3 - Spread out runner contacted_at updates diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index f3dc314a89..e2c5178185 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -9,9 +9,6 @@ - if tag.message.present?   = strip_gpg_signature(tag.message) - - if release -   - %span.label.label-success release .controls = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do From ed757ef6e3aab55c8fe0f2c1efdd79d65025c550 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 14:44:08 +0100 Subject: [PATCH 393/443] Rewrite remove tag test Signed-off-by: Dmitriy Zaporozhets --- features/project/commits/tags.feature | 9 +-------- features/steps/project/commits/tags.rb | 25 ++++++------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature index 02f399f7ca..24fb84d1cc 100644 --- a/features/project/commits/tags.feature +++ b/features/project/commits/tags.feature @@ -29,13 +29,6 @@ Feature: Project Commits Tags @javascript Scenario: I delete a tag + Given I visit tag 'v1.1.0' page Given I delete tag 'v1.1.0' Then I should not see tag 'v1.1.0' - - @javascript - Scenario: I delete all tags and see info message - Given I delete all tags - Then I should see tags info message - - # @wip - # Scenario: I can download project by tag diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index e6f8faf50f..ff824c7695 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -52,11 +52,13 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps expect(page).to have_content 'Tag already exists' end + step "I visit tag 'v1.1.0' page" do + click_link 'v1.1.0' + end + step "I delete tag 'v1.1.0'" do - page.within '.tags' do - first('.btn-remove').click - sleep 0.05 - end + first('.btn-remove').click + sleep 0.05 end step "I should not see tag 'v1.1.0'" do @@ -64,19 +66,4 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps expect(page.all(visible: true)).not_to have_content 'v1.1.0' end end - - step 'I delete all tags' do - page.within '.tags' do - page.all('.btn-remove').each do |remove| - remove.click - sleep 0.05 - end - end - end - - step 'I should see tags info message' do - page.within '.tags' do - expect(page).to have_content 'Repository has no tags yet.' - end - end end From c1a893d91a569cf65b381e8c2b56c82824e9f593 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 14:44:46 +0100 Subject: [PATCH 394/443] Fix list rendering issue when dropdown was hidden in row Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 2 +- app/assets/stylesheets/framework/mixins.scss | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index f6942db581..45f3b5849b 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -117,7 +117,7 @@ ul.content-list { } .controls { - padding-top: 4px; + padding-top: 1px; float: right; .btn { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index b9c179f288..11c48d26ab 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -72,9 +72,10 @@ list-style: none; > li { + @include clearfix; + padding: 10px 0; border-bottom: 1px solid #EEE; - overflow: hidden; display: block; margin: 0px; From b60ad399acdd753efe4f2d724c47800b6a70056b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 14:47:29 +0100 Subject: [PATCH 395/443] Fix test Signed-off-by: Dmitriy Zaporozhets --- features/project/commits/tags.feature | 1 - features/steps/project/commits/tags.rb | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature index 24fb84d1cc..660238c231 100644 --- a/features/project/commits/tags.feature +++ b/features/project/commits/tags.feature @@ -27,7 +27,6 @@ Feature: Project Commits Tags And I submit new tag form with tag that already exists Then I should see new an error that tag already exists - @javascript Scenario: I delete a tag Given I visit tag 'v1.1.0' page Given I delete tag 'v1.1.0' diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index ff824c7695..cb5fe20834 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -57,13 +57,14 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step "I delete tag 'v1.1.0'" do - first('.btn-remove').click - sleep 0.05 + page.within('.content') do + first('.btn-remove').click + end end step "I should not see tag 'v1.1.0'" do page.within '.tags' do - expect(page.all(visible: true)).not_to have_content 'v1.1.0' + expect(page).not_to have_link 'v1.1.0' end end end From 962b57df5a3f476407858b12455030d8cda66e4b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 15:05:19 +0100 Subject: [PATCH 396/443] Small UI improvements to new git tag page Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/releases/edit.html.haml | 4 +++- app/views/projects/tags/new.html.haml | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index fb841b77a2..7874141634 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -3,7 +3,9 @@ .gray-content-block .oneline - Release notes for #{@tag.name} + .title + Release notes for tag + %strong #{@tag.name} .prepend-top-default = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f| diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 9b224ff89b..be8f2e6f70 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -5,9 +5,11 @@ .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × = @error + %h3.page-title - %i.fa.fa-code-fork - New tag + New git tag +%hr + = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal tag-form" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' @@ -17,15 +19,15 @@ = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control' - .light Branch name or commit SHA + .help-block Branch name or commit SHA .form-group = label_tag :message, 'Message', class: 'control-label' .col-sm-10 = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' - .light (Optional) Entering a message will create an annotated tag. + .help-block (Optional) Entering a message will create an annotated tag. %hr .form-group - = label_tag :release_description, 'Release description', class: 'control-label' + = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do .zennable @@ -39,7 +41,7 @@ = icon('compress') = render 'projects/notes/hints' - .help-block You can add release description to your tag. It will be stored in GitLab database and displayed on tags page + .help-block (Optional) You can add release notes to your tag. It will be stored in GitLab database and displayed on tags page .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' From cb8b9c3fe2caba14752978f4283affc07581087c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 15:29:04 +0100 Subject: [PATCH 397/443] Add association and validation for Release model Signed-off-by: Dmitriy Zaporozhets --- app/models/release.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/release.rb b/app/models/release.rb index 1dc9ce6dd4..05647839e8 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,2 +1,5 @@ class Release < ActiveRecord::Base + belongs_to :project + + validates :description, :project, presence: true end From 6f15356aea488ce0085c982aac2c97cdd46db96b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 15:43:59 +0100 Subject: [PATCH 398/443] Add tests to release notes feature Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/tags/show.html.haml | 6 +++--- features/project/commits/tags.feature | 12 ++++++++++++ features/steps/project/commits/tags.rb | 20 ++++++++++++++++++++ spec/factories/releases.rb | 6 +++--- spec/models/release_spec.rb | 13 ++++++++++++- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index f80ba5516a..f95ae9edc4 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -4,11 +4,11 @@ .gray-content-block .pull-right - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn' do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn', title: 'Edit release notes' do = icon("pencil") - = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse source code' do = icon('files-o') - = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped' do + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse commits' do = icon('history') - if can? current_user, :download_code, @project = render 'projects/tags/download', ref: @tag.name, project: @project diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature index 660238c231..56ee091acc 100644 --- a/features/project/commits/tags.feature +++ b/features/project/commits/tags.feature @@ -12,6 +12,12 @@ Feature: Project Commits Tags And I submit new tag form Then I should see new tag created + Scenario: I create a tag with release notes + Given I click new tag link + And I submit new tag form with release notes + Then I should see new tag created + And I should see tag release notes + Scenario: I create a tag with invalid name And I click new tag link And I submit new tag form with invalid name @@ -31,3 +37,9 @@ Feature: Project Commits Tags Given I visit tag 'v1.1.0' page Given I delete tag 'v1.1.0' Then I should not see tag 'v1.1.0' + + Scenario: I add release notes to the tag + Given I visit tag 'v1.1.0' page + When I click edit tag link + And I fill release notes and submit form + Then I should see tag release notes diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index cb5fe20834..eff4234a44 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -18,6 +18,18 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps click_button 'Create tag' end + step 'I submit new tag form with release notes' do + fill_in 'tag_name', with: 'v7.0' + fill_in 'ref', with: 'master' + fill_in 'release_description', with: 'Awesome release notes' + click_button 'Create tag' + end + + step 'I fill release notes and submit form' do + fill_in 'release_description', with: 'Awesome release notes' + click_button 'Save changes' + end + step 'I submit new tag form with invalid name' do fill_in 'tag_name', with: 'v 1.0' fill_in 'ref', with: 'master' @@ -67,4 +79,12 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps expect(page).not_to have_link 'v1.1.0' end end + + step 'I click edit tag link' do + click_link 'Edit release notes' + end + + step 'I should see tag release notes' do + expect(page).to have_content 'Awesome release notes' + end end diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 067d8138e4..80d6bbee6c 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -2,8 +2,8 @@ FactoryGirl.define do factory :release do - tag "MyString" - description "MyText" - project_id 1 + tag "v1.1.0" + description "Awesome release" + project end end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index e533734ba0..527005b2b6 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -1,5 +1,16 @@ require 'rails_helper' RSpec.describe Release, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + let(:release) { create(:release) } + + it { expect(release).to be_valid } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:description) } + end end From 14518ba65a7727583445314917d466dabe4a6811 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 15:48:18 +0100 Subject: [PATCH 399/443] Better english Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/tags/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index be8f2e6f70..e106be794f 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -41,7 +41,7 @@ = icon('compress') = render 'projects/notes/hints' - .help-block (Optional) You can add release notes to your tag. It will be stored in GitLab database and displayed on tags page + .help-block (Optional) You can add release notes to your tag. It will be stored in the GitLab database and shown on the tags page .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' From c5ec2a23a466f45127b6056693e1a473ae7e503e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Nov 2015 16:27:18 +0100 Subject: [PATCH 400/443] Small UI improvements to merge request page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/blocks.scss | 4 ++++ app/assets/stylesheets/pages/diff.scss | 1 - app/helpers/diff_helper.rb | 4 ++-- app/views/projects/diffs/_diffs.html.haml | 2 +- app/views/projects/merge_requests/_discussion.html.haml | 2 +- app/views/projects/merge_requests/show/_commits.html.haml | 4 ++++ 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 8917c53b1f..1635df9c97 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -28,6 +28,10 @@ border-bottom: 1px solid $border-color; color: $gl-gray; + &.oneline-block { + line-height: 42px; + } + &.white { background-color: white; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d9ef06dc6b..afd6fb7367 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -367,7 +367,6 @@ .inline-parallel-buttons { float: right; - margin-top: -5px; } // Mobile diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e65e37211c..b889fb2897 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -137,7 +137,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do + link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do 'Inline' end end @@ -148,7 +148,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do + link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do 'Side-by-side' end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 56b51f038b..e46bf1ab1e 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs) -.gray-content-block.second-block +.gray-content-block.second-block.oneline-block .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 38e66c3828..7e60782ff5 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -7,7 +7,7 @@ = render 'shared/show_aside' -.gray-content-block.second-block +.gray-content-block.second-block.oneline-block .row .col-md-9 .votes-holder.pull-right diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a71b181a6a..478054db51 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1 +1,5 @@ +.gray-content-block.second-block.oneline-block + = icon("sort-amount-desc") + Most recent commits displayed first + = render "projects/commits/commits", project: @merge_request.project From 8f2561b193ad39f116655af0789798b45ad906c8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 6 Nov 2015 08:04:02 -0800 Subject: [PATCH 401/443] Add spec for manual merge of merge request --- .../merge_requests/refresh_service_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 227ac995ec..7ee4488521 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -62,6 +62,25 @@ describe MergeRequests::RefreshService do it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } end + context 'manual merge of source branch' do + before do + # Merge master -> feature branch + author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } + commit_options = { message: 'Test message', committer: author, author: author } + master_commit = @project.repository.commit('master') + @project.repository.merge(@user, master_commit.id, 'feature', commit_options) + commit = @project.repository.commit('feature') + service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') + reload_mrs + end + + it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request).to be_merged } + it { expect(@merge_request.diffs.length).to be > 0 } + it { expect(@fork_merge_request).to be_merged } + it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + end + context 'push to fork repo source branch' do let(:refresh_service) { service.new(@fork_project, @user) } before do From e34904370f7383853ed9e1f5392ff76defda9530 Mon Sep 17 00:00:00 2001 From: Baldinof Date: Fri, 6 Nov 2015 23:45:33 +0100 Subject: [PATCH 402/443] Merge button has color from CI status --- app/assets/stylesheets/pages/merge_requests.scss | 14 ++++++++++++++ .../merge_requests/widget/open/_accept.html.haml | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f0b3667acc..08e4bcdf52 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -19,6 +19,20 @@ .accept-merge-holder { .accept-action { display: inline-block; + + .accept_merge_request { + &.ci-pending, + &.ci-running { + @include btn-orange; + } + + &.ci-skipped, + &.ci-failed, + &.ci-canceled, + &.ci-error { + @include btn-red; + } + } } .accept-control { diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 613525437a..689247f318 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,8 +1,10 @@ +- status_class = @merge_request.ci_commit ? " ci-#{@merge_request.ci_commit.status}" : nil + = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container .accept-action - = f.button class: "btn btn-create accept_merge_request" do + = f.button class: "btn btn-create accept_merge_request#{status_class}" do Accept Merge Request - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? .accept-control.checkbox From b14198a5e7520ce7cf356006507db9b190e3afe3 Mon Sep 17 00:00:00 2001 From: Christian Speich Date: Sat, 7 Nov 2015 12:40:14 +0100 Subject: [PATCH 403/443] Hide tab-bar in login-box when only one tabs is shown. --- app/views/devise/shared/_signin_box.html.haml | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 41ad2c231d..9a1331b254 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -7,26 +7,34 @@ %h3 Sign in .login-body - if form_based_providers.any? - %ul.nav.nav-tabs + - if form_based_providers.count >= 2 || signin_enabled? + %ul.nav.nav-tabs + - if crowd_enabled? + %li.active + = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero? && !crowd_enabled?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if signin_enabled? + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + .tab-content + - if crowd_enabled? + %div.tab-pane.active{id: "tab-crowd"} + = render 'devise/sessions/new_crowd' + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' + - else - if crowd_enabled? - %li.active - = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' - - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - - if signin_enabled? - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' - .tab-content - - if crowd_enabled? - %div.tab-pane.active{id: "tab-crowd"} - = render 'devise/sessions/new_crowd' - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} - = render 'devise/sessions/new_ldap', server: server - - if signin_enabled? - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' + = render 'devise/sessions/new_crowd' + - elsif @ldap_servers.any? + = render 'devise/sessions/new_ldap', server: @ldap_servers.first + - elsif signin_enabled? + = render 'devise/sessions/new_base' - elsif signin_enabled? = render 'devise/sessions/new_base' From 920a2d974c15662ffac49146348c8d638a9db92b Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 28 Oct 2015 15:58:40 +0100 Subject: [PATCH 404/443] Fixed markdown issue in PROCESS.md --- PROCESS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PROCESS.md b/PROCESS.md index d42168a723..a4b0c83644 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -119,6 +119,6 @@ rebase with master to see if that solves the issue. ### Closing down the issue tracker on GitHub We are currently in the process of closing down the issue tracker on GitHub, to -prevent duplication with the [GitLab.com issue tracker][https://gitlab.com/gitlab-org/gitlab-ce/issues]. +prevent duplication with the GitLab.com issue tracker. Since this is an older issue I'll be closing this for now. If you think this is -still an issue I encourage you to open it on the [GitLab.com issue tracker][https://gitlab.com/gitlab-org/gitlab-ce/issues]. +still an issue I encourage you to open it on the \[GitLab.com issue tracker\](https://gitlab.com/gitlab-org/gitlab-ce/issues). From 4099751ce9b33fbafbfcfbf1506e091a20f7c034 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 7 Nov 2015 20:22:16 +0100 Subject: [PATCH 405/443] Render same markdown hint for issue, merge request, wiki and comment forms Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/markdown_area.scss | 1 + app/assets/stylesheets/pages/note_form.scss | 6 +++++- app/views/projects/wikis/_form.html.haml | 4 +--- app/views/shared/issuable/_form.html.haml | 9 +-------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ed0333d233..cc660529cb 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -106,6 +106,7 @@ } .markdown-area { + @include border-radius(0); background: #FFF; border: 1px solid #ddd; min-height: 140px; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4392f08942..268fc995aa 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -56,6 +56,10 @@ .note_text { width: 100%; } + + .comment-hints { + margin-top: -12px; + } } /* loading indicator */ @@ -168,7 +172,7 @@ color: #999; background: #FFF; padding: 7px; - margin-top: -11px; + margin-top: -7px; border: 1px solid $border-color; font-size: 13px; } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 261d4a92d7..9c94c43e74 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -23,9 +23,7 @@ .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit' - .col-sm-12.hint - .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} - .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render 'projects/notes/hints' .clearfix .error-alert diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 594e54f404..0fc74d7d2b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -27,14 +27,7 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' - .col-sm-12.hint - .pull-left - Parsed with - #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach files by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector' }. - + = render 'projects/notes/hints' .clearfix .error-alert %hr From b8cd6f9aae2eb0753c8f36284331aef6b4557bab Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 8 Nov 2015 18:00:31 +0200 Subject: [PATCH 406/443] Update piwik template --- app/views/layouts/_piwik.html.haml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml index 135e8daca2..259b4f7cdf 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -1,12 +1,14 @@ + :javascript var _paq = _paq || []; - _paq.push(["trackPageView"]); - _paq.push(["enableLinkTracking"]); - + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); (function() { - var u=(("https:" == document.location.protocol) ? "https" : "http") + "://#{extra_config.piwik_url}/"; - _paq.push(["setTrackerUrl", u+"piwik.php"]); - _paq.push(["setSiteId", "#{extra_config.piwik_site_id}"]); - var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0]; g.type="text/javascript"; - g.defer=true; g.async=true; g.src=u+"piwik.js"; s.parentNode.insertBefore(g,s); + var u="//#{extra_config.piwik_url}/"; + _paq.push(['setTrackerUrl', u+'piwik.php']); + _paq.push(['setSiteId', #{extra_config.piwik_site_id}]); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); +

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≀fzN-YZ2W)&knUR7x7xbOJhd+V|a?RK-e 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