From 9d594380945a17a30d586c36db7867595950feb8 Mon Sep 17 00:00:00 2001 From: Ted Strzalkowski Date: Tue, 21 Jul 2015 13:17:43 -0700 Subject: [PATCH 001/543] Removes reference to Enterprise Edition ssh key sync feature. --- config/gitlab.yml.example | 1 - 1 file changed, 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c32ac2042d..69671e42a5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -361,7 +361,6 @@ test: user_filter: '' group_base: 'ou=groups,dc=example,dc=com' admin_group: '' - sync_ssh_keys: false staging: <<: *base From 4d0423cd9fd3eb62a8d61881ea8a02322eef4a4d Mon Sep 17 00:00:00 2001 From: karen Carias Date: Tue, 25 Aug 2015 22:15:02 -0700 Subject: [PATCH 002/543] New doc about how to create an issue --- doc/gitlab-basics/README.md | 2 ++ doc/gitlab-basics/create-issue.md | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 doc/gitlab-basics/create-issue.md diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index b904c70e98..493e1d1b09 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -23,3 +23,5 @@ Step-by-step guides on the basics of working with Git and GitLab. * [Add an image](add-image.md) * [Create a Merge Request](add-merge-request.md) + +* [Create an Issue](create-issue.md) diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md new file mode 100644 index 0000000000..a272e84d4f --- /dev/null +++ b/doc/gitlab-basics/create-issue.md @@ -0,0 +1,27 @@ +# How to create an Issue in GitLab + +The Issue Tracker is a good place to add things that need to be improved or solved in a project. + +To create an Issue, sign in to GitLab. + +Go to the project where you'd like to create the Issue: + +![Select a project](basicsimages/select_project.png) + +Click on "Issues" on the left side of your screen: + +![Issues](basicsimages/issues.png) + +Click on the "+ new issue" button on the right side of your screen: + +![New issue](basicsimages/new_issue.png) + +Add a title and a description to your issue: + +![Issue title and description](basicsimages/issue_title.png) + +You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue": + +![Submit new issue](basicsimages/submit_new_issue.png) + +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. From 92e15cfb82620ff85558d4a8f1a5b84dacabcab4 Mon Sep 17 00:00:00 2001 From: karen Carias Date: Mon, 31 Aug 2015 23:01:25 -0700 Subject: [PATCH 003/543] more info about MR and mentions --- doc/gitlab-basics/create-issue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index a272e84d4f..87f078def0 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html). From 454d227b45a563cb21ea8fa5091e687a2876380f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 11 Aug 2015 01:44:40 -0400 Subject: [PATCH 004/543] 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 005/543] 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 006/543] 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 007/543] 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 008/543] 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 009/543] 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 010/543] 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 011/543] 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 012/543] 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 013/543] 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 014/543] 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 015/543] 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 016/543] 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 4743b8bd6a6c1556c1a3163b438de90360e2e374 Mon Sep 17 00:00:00 2001 From: karen Carias Date: Tue, 8 Sep 2015 18:36:11 -0700 Subject: [PATCH 017/543] link to rake tasks --- doc/raketasks/backup_restore.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 6a68c8e828..b0ea1a9753 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -7,7 +7,9 @@ A backup creates an archive file that contains the database, all repositories and all attachments. This archive will be saved in backup_path (see `config/gitlab.yml`). The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup. -You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. +You can only restore a backup to exactly the same version of GitLab that you created it +on, for example 7.2.1. The best way to migrate your repositories from one server to +another is through backup restore. You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` (for omnibus packages) or `/home/git/gitlab/.secret` (for installations @@ -370,3 +372,6 @@ For more information see similar questions on postgresql issue tracker[here](htt ## Note This documentation is for GitLab CE. We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. + +To migrate your repositories from one server to another with an up-to-date version of +GitLab, you can use [rake tasks](import.md) to do a mass import of the repository. From 6a1d695f861e4c5251a2333c673f78705b34891f Mon Sep 17 00:00:00 2001 From: karen Carias Date: Wed, 9 Sep 2015 15:17:41 -0700 Subject: [PATCH 018/543] made info more clear --- doc/raketasks/backup_restore.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index b0ea1a9753..ad1914398d 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -374,4 +374,6 @@ This documentation is for GitLab CE. We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. To migrate your repositories from one server to another with an up-to-date version of -GitLab, you can use [rake tasks](import.md) to do a mass import of the repository. +GitLab, you can use the [import rake task](import.md) to do a mass import of the +repository. Note that if you do an import rake task, rather than a backup restore, you +will have all your repositories, but not any other data. From 52d7813f2eb554d82b6c435a3207fc2e59c76e70 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 15 Sep 2015 16:10:29 +0200 Subject: [PATCH 019/543] Improve repo cleanup task Clean up more than just global (legacy) repos. Also, instead of deleting, just rename. --- lib/tasks/gitlab/cleanup.rake | 45 +++++++++-------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 6b1e371614..f97d4b86e0 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -46,43 +46,20 @@ namespace :gitlab do desc "GitLab | Cleanup | Clean repositories" task repos: :environment do warn_user_is_not_gitlab - remove_flag = ENV['REMOVE'] - git_base_path = Gitlab.config.gitlab_shell.repos_path - all_dirs = Dir.glob(git_base_path + '/*') - - global_projects = Project.in_namespace(nil).pluck(:path) - - puts git_base_path.yellow - puts "Looking for global repos to remove... " - - # skip non git repo - all_dirs.select! do |dir| - dir =~ /.git$/ - end - - # skip existing repos - all_dirs.reject! do |dir| - repo_name = File.basename dir - path = repo_name.gsub(/\.git$/, "") - global_projects.include?(path) - end - - all_dirs.each do |dir_path| - if remove_flag - if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".red - else - puts "Cannot remove #{dir_path}".red - end - else - puts "Can be removed: #{dir_path}".red + move_suffix = "+orphaned+#{Time.now.to_i}" + repo_root = Gitlab.config.gitlab_shell.repos_path + # Look for global repos (legacy, depth 1) and normal repos (depth 2) + IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| + find.each_line do |path| + path.chomp! + repo_with_namespace = path.sub(repo_root + '/', '').chomp('.git').chomp('.wiki') + next if Project.find_with_namespace(repo_with_namespace) + new_path = path + move_suffix + puts path.inspect + ' -> ' + new_path.inspect + File.rename(path, new_path) end end - - unless remove_flag - puts "To cleanup this directories run this command with REMOVE=true".yellow - end end desc "GitLab | Cleanup | Block users that have been removed in LDAP" From ed38627ec55ce4c3352f44167c4fc75c59841315 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 15 Sep 2015 17:07:51 +0200 Subject: [PATCH 020/543] Guard against trailing slashes in repos_path --- lib/tasks/gitlab/cleanup.rake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index f97d4b86e0..9f5852ac61 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -53,7 +53,11 @@ namespace :gitlab do IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| find.each_line do |path| path.chomp! - repo_with_namespace = path.sub(repo_root + '/', '').chomp('.git').chomp('.wiki') + repo_with_namespace = path. + sub(repo_root, ''). + sub(%r{^/*}, ''). + chomp('.git'). + chomp('.wiki') next if Project.find_with_namespace(repo_with_namespace) new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect From 8d0219f0da6419fe8a2e71f3e7ccf31437b5cb6b Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Fri, 18 Sep 2015 11:08:37 +0200 Subject: [PATCH 021/543] Make Visit project on GitLab button goes to The internal Gitlab project Since we are merging CI into CE, projects should go to the internal Projects page, we can also use Turbolinks to do so. In order to give the user an idea of integration, I renamed the button: "Go to Project" Signed-off-by: Jeroen van Baarsen --- app/views/ci/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml index 6443378af9..c479629604 100644 --- a/app/views/ci/projects/show.html.haml +++ b/app/views/ci/projects/show.html.haml @@ -17,7 +17,7 @@ = link_to @ref, ci_project_path(@project, ref: @ref) %li.pull-right - = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' ) + = link_to 'Go to project', project_path(gl_project), class: 'btn btn-sm' - if @ref %p From 322913f331dddc811760f7d42201974b02aa9f5e Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Thu, 17 Sep 2015 16:50:20 +0200 Subject: [PATCH 022/543] Fix URL targets for uploads, markdown preview and autocomplete in MR textareas --- CHANGELOG | 1 + app/views/layouts/_init_auto_complete.html.haml | 3 ++- app/views/layouts/project.html.haml | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7a54700af0..6b533bffd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ v 8.0.0 (unreleased) - Webhook for issue now contains repository field (Jungkook Park) - Add ability to add custom text to the help page (Jeroen van Baarsen) - Add pg_schema to backup config + - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato) v 7.14.3 - No changes diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 3c58f10e75..035fe0056d 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,3 +1,4 @@ +- project = @target_project || @project :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(@project.namespace, @project, type: @noteable.class, type_id: params[:id])}" + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}" GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 78dafcd8bf..abf73bcc70 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -3,10 +3,11 @@ - sidebar "project" unless sidebar - content_for :scripts_body_top do + - project = @target_project || @project - if current_user :javascript - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; - window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}"; + window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; + window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}"; - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user From 69cb5fba59a86af0e3311c3ff3e00d0c391797f0 Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Thu, 17 Sep 2015 15:44:59 +0200 Subject: [PATCH 023/543] Make Project#find_with_namespace case-insensitive --- app/models/project.rb | 4 ++-- spec/models/project_spec.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 6e2f964566..3b233f2b89 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -235,10 +235,10 @@ class Project < ActiveRecord::Base return nil unless id.include?('/') id = id.split('/') - namespace = Namespace.find_by(path: id.first) + namespace = Namespace.by_path(id.first) return nil unless namespace - where(namespace_id: namespace.id).find_by(path: id.second) + where(namespace_id: namespace.id).where("LOWER(projects.path) = :path", path: id.second.downcase).first end def visibility_levels diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fcbd5ae10..5aaa3af4d2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -220,6 +220,7 @@ describe Project do end it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) } + it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) } it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } end end From 6bff207dfdb37618f0f4291abf40d25127836dfe Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Thu, 17 Sep 2015 16:06:55 +0200 Subject: [PATCH 024/543] Refactor spec to go through ApplicationController#project --- spec/controllers/projects/issues_controller_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 871b9219ca..dd45e84f74 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -8,12 +8,11 @@ describe Projects::IssuesController do before do sign_in(user) project.team << [user, :developer] - controller.instance_variable_set(:@project, project) end describe "GET #index" do it "returns index" do - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(200) end @@ -22,14 +21,15 @@ describe Projects::IssuesController do project.issues_enabled = false project.save - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end it "returns 404 when external issue tracker is enabled" do + controller.instance_variable_set(:@project, project) allow(project).to receive(:default_issues_tracker?).and_return(false) - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end From 1bbcc29687e9d5cef52e630f1e85d4c9f81fca07 Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Thu, 17 Sep 2015 15:56:16 +0200 Subject: [PATCH 025/543] Redirect case sensitive project path to the normalized one --- app/controllers/application_controller.rb | 7 ++++++- .../controllers/projects/issues_controller_spec.rb | 6 ++++++ spec/controllers/projects_controller_spec.rb | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9b6472a7b1..527c9da0fa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,9 +117,14 @@ class ApplicationController < ActionController::Base redirect_to request.original_url.gsub(/\.git\Z/, '') and return end - @project = Project.find_with_namespace("#{namespace}/#{id}") + project_path = "#{namespace}/#{id}" + @project = Project.find_with_namespace(project_path) + if @project and can?(current_user, :read_project, @project) + if @project.path_with_namespace != project_path + redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return + end @project elsif current_user.nil? @project = nil diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index dd45e84f74..76d56bc989 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -17,6 +17,12 @@ describe Projects::IssuesController do expect(response.status).to eq(200) end + it "return 301 if request path doesn't match project path" do + get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + + expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) + end + it "returns 404 when issues are disabled" do project.issues_enabled = false project.save diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 29233e9fae..33eedc4dc8 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -21,6 +21,20 @@ describe ProjectsController do expect(response.body).to include("content='#{content}'") end 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 + + expect(response).to redirect_to("/#{public_project.path_with_namespace}") + end + + it "loads the page if normalized path matches request path" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + + expect(response.status).to eq(200) + end + end end describe "POST #toggle_star" do From f1ae825015220685ce020fbc1a3f00155f809111 Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Thu, 17 Sep 2015 16:15:51 +0200 Subject: [PATCH 026/543] Update Changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 7a54700af0..ac46633f40 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ v 8.0.0 (unreleased) - Webhook for issue now contains repository field (Jungkook Park) - Add ability to add custom text to the help page (Jeroen van Baarsen) - Add pg_schema to backup config + - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato) v 7.14.3 - No changes From 333463ddf280180d1878186160eb5a5e9c5c6e3e Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Fri, 18 Sep 2015 00:04:38 +0200 Subject: [PATCH 027/543] Fix indentation --- spec/controllers/projects_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 33eedc4dc8..21beaf37fc 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -24,9 +24,9 @@ describe ProjectsController do 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 + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase - expect(response).to redirect_to("/#{public_project.path_with_namespace}") + expect(response).to redirect_to("/#{public_project.path_with_namespace}") end it "loads the page if normalized path matches request path" do From 57b009829d3139b2c42d516dc0645fe6bde0294c Mon Sep 17 00:00:00 2001 From: karen Carias Date: Sat, 19 Sep 2015 19:28:18 -0700 Subject: [PATCH 028/543] note about EE git hooks --- doc/hooks/custom_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index f7d4f3de68..dd3be70c90 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore webhooks as an option if you do not have filesystem access.** +Please explore webhooks as an option if you do not have filesystem access. For a user-friendly Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. From bd6c982bf1dce111d8aa17d7c4c5acd073051a38 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 24 Sep 2015 12:03:23 +0000 Subject: [PATCH 029/543] Documentation of omniauth-ldap limitations Further documentation about limitations directly impacting settings of users' LDAP servers. --- doc/integration/ldap.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 3bc5df21ef..9b7d8fa396 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -173,3 +173,23 @@ Tip: if you want to limit access to the nested members of an Active Directory gr ``` Please note that GitLab does not support the custom filter syntax used by omniauth-ldap. + +## Limitations + +GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap) +which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation +of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP +server settings. + +### TLS Client Authentication +Not implemented by `Net::LDAP`. +So you should disable anonymous LDAP authentication and enable simple or SASL +authentication. TLS client authentication setting in your LDAP server cannot be +mandatory and clients cannot be authenticated with the TLS protocol. + +### TLS Server Authentication +Not supported by GitLab's configuration options. +When setting `method: ssl`, the underlying authentication method used by +`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with +the LDAP server before any LDAP-protocol data is exchanged but no validation of +the LDAP server's SSL certificate is performed. \ No newline at end of file From 83347954fc3c0c317c77f0528cdbaa456093771a Mon Sep 17 00:00:00 2001 From: Pavel Forkert Date: Thu, 24 Sep 2015 16:14:16 +0300 Subject: [PATCH 030/543] Add option to admin area to sign in as a specific user Closes #2291 --- app/controllers/admin/users_controller.rb | 6 ++++++ app/views/admin/users/index.html.haml | 3 ++- config/routes.rb | 1 + spec/controllers/admin/users_controller_spec.rb | 15 +++++++++++++++ spec/features/admin/admin_users_spec.rb | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a19b1abee2..00f41a10dd 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -63,6 +63,12 @@ class Admin::UsersController < Admin::ApplicationController end end + def login_as + sign_in(user) + flash[:alert] = "Logged in as #{user.username}" + redirect_to root_path + end + def disable_two_factor user.disable_two_factor! redirect_to admin_user_path(user), diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index e3698ac1c4..8dbce7a4a1 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -72,7 +72,7 @@ = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" %ul.well-list - @users.each do |user| - %li + %li{ class: "user-#{user.id}" } .list-item-name - if user.blocked? %i.fa.fa-lock.cred @@ -90,6 +90,7 @@   = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - unless user == current_user + = link_to 'Log in', login_as_admin_user_path(user), method: :put, class: "btn btn-xs btn-primary" - if user.blocked? = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - else diff --git a/config/routes.rb b/config/routes.rb index 4a07c449b4..5f7d06a620 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -262,6 +262,7 @@ Gitlab::Application.routes.draw do put :unblock put :unlock put :confirm + put :login_as patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index c40b2c2a58..e4c32cd2a1 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -7,6 +7,21 @@ describe Admin::UsersController do sign_in(admin) end + describe 'PUT login_as' do + let(:user) { create(:user) } + + it 'logs admin as another user' do + expect(warden.authenticate(scope: :user)).not_to eq(user) + put :login_as, id: user.username + expect(warden.authenticate(scope: :user)).to eq(user) + end + + it 'redirects user to homepage' do + put :login_as, id: user.username + expect(response).to redirect_to(root_path) + end + end + describe 'DELETE #user with projects' do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 8671776158..870a82d0ee 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -166,4 +166,20 @@ describe "Admin::Users", feature: true do end end end + + it 'should be able to log in as another user' do + another_user = create(:user) + + visit admin_users_path + + page.within ".user-#{another_user.id}" do + click_link 'Log in' + end + + expect(page).to have_content("Logged in as #{another_user.username}") + + page.within '.sidebar-user .username' do + expect(page).to have_content(another_user.username) + end + end end From eb9528b8b964c78ef3d33818286c529b83c35a5e Mon Sep 17 00:00:00 2001 From: Pavel Forkert Date: Thu, 24 Sep 2015 16:34:04 +0300 Subject: [PATCH 031/543] Move login button to user page, switched to POST method --- app/views/admin/users/_head.html.haml | 2 + app/views/admin/users/index.html.haml | 1 - config/routes.rb | 2 +- .../admin/users_controller_spec.rb | 6 +-- spec/features/admin/admin_users_spec.rb | 37 +++++++++++-------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9d5e934c8b..4245d0f1ed 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,6 +6,8 @@ %span.cred (Admin) .pull-right + - unless @user == current_user + = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 8dbce7a4a1..82a88863eb 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -90,7 +90,6 @@   = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - unless user == current_user - = link_to 'Log in', login_as_admin_user_path(user), method: :put, class: "btn btn-xs btn-primary" - if user.blocked? = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - else diff --git a/config/routes.rb b/config/routes.rb index 5f7d06a620..0792cb559e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -262,7 +262,7 @@ Gitlab::Application.routes.draw do put :unblock put :unlock put :confirm - put :login_as + post :login_as patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index e4c32cd2a1..7168db117d 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -7,17 +7,17 @@ describe Admin::UsersController do sign_in(admin) end - describe 'PUT login_as' do + describe 'POST login_as' do let(:user) { create(:user) } it 'logs admin as another user' do expect(warden.authenticate(scope: :user)).not_to eq(user) - put :login_as, id: user.username + post :login_as, id: user.username expect(warden.authenticate(scope: :user)).to eq(user) end it 'redirects user to homepage' do - put :login_as, id: user.username + post :login_as, id: user.username expect(response).to redirect_to(root_path) end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 870a82d0ee..67da3c199a 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -111,6 +111,27 @@ describe "Admin::Users", feature: true do expect(page).to have_content(@user.name) end + describe 'Login as another user' do + it 'should show login button for other users' do + another_user = create(:user) + + visit admin_user_path(another_user) + + click_link 'Log in as this user' + + expect(page).to have_content("Logged in as #{another_user.username}") + + page.within '.sidebar-user .username' do + expect(page).to have_content(another_user.username) + end + end + + it 'should not show login button for admin itself' do + visit admin_user_path(@user) + expect(page).not_to have_content('Log in as this user') + end + end + describe 'Two-factor Authentication status' do it 'shows when enabled' do @user.update_attribute(:two_factor_enabled, true) @@ -166,20 +187,4 @@ describe "Admin::Users", feature: true do end end end - - it 'should be able to log in as another user' do - another_user = create(:user) - - visit admin_users_path - - page.within ".user-#{another_user.id}" do - click_link 'Log in' - end - - expect(page).to have_content("Logged in as #{another_user.username}") - - page.within '.sidebar-user .username' do - expect(page).to have_content(another_user.username) - end - end end From 82eeb5e284bd22bc04c82def521cc3d65eb2bcd1 Mon Sep 17 00:00:00 2001 From: Pavel Forkert Date: Thu, 24 Sep 2015 16:38:52 +0300 Subject: [PATCH 032/543] Remove stuff from previous UI --- app/views/admin/users/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 82a88863eb..e3698ac1c4 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -72,7 +72,7 @@ = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" %ul.well-list - @users.each do |user| - %li{ class: "user-#{user.id}" } + %li .list-item-name - if user.blocked? %i.fa.fa-lock.cred From 3dec9dc4a36db340a61fc2a0fbdc056957b0279f Mon Sep 17 00:00:00 2001 From: Pavel Forkert Date: Thu, 24 Sep 2015 16:40:21 +0300 Subject: [PATCH 033/543] Clarify spec title explanation --- spec/features/admin/admin_users_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 67da3c199a..c2c7364f6c 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -112,7 +112,7 @@ describe "Admin::Users", feature: true do end describe 'Login as another user' do - it 'should show login button for other users' do + it 'should show login button for other users and check that it works' do another_user = create(:user) visit admin_user_path(another_user) From 0c877875ea3e7ccbdf48b7736f8c35e2a179ab45 Mon Sep 17 00:00:00 2001 From: Pavel Forkert Date: Thu, 24 Sep 2015 16:42:27 +0300 Subject: [PATCH 034/543] Add entry to changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4a34a3835a..c19e36ed47 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) + - Add option to admin area to sign in as a specific user (Pavel Forkert) - Show CI status on all pages where commits list is rendered - Automatically enable CI when push .gitlab-ci.yml file to repository - Move CI charts to project graphs area From 1f78c5d46053ad37c280f0bc5273522d76888e63 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 24 Sep 2015 18:36:15 +0200 Subject: [PATCH 035/543] UI changes to the project view, empty project and project list new project button --- app/assets/stylesheets/base/mixins.scss | 81 ++++- app/assets/stylesheets/base/variables.scss | 4 +- app/assets/stylesheets/generic/avatar.scss | 2 + .../stylesheets/generic/typography.scss | 22 +- app/assets/stylesheets/pages/projects.scss | 296 ++++++++++++++++-- app/views/projects/_home_panel.html.haml | 27 +- app/views/projects/buttons/_fork.html.haml | 1 - app/views/projects/buttons/_star.html.haml | 5 - app/views/shared/_clone_panel.html.haml | 2 +- 9 files changed, 372 insertions(+), 68 deletions(-) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index a2f6c3e21f..cdebe49891 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -93,46 +93,89 @@ } h1 { - margin-top: 45px; - font-size: 2.5em; + font-size: 1.3em; + font-weight: 600; + margin: 24px 0 12px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #e7e9ed; + color: #313236; } h2 { - margin-top: 40px; - font-size: 2em; + font-size: 1.2em; + font-weight: 600; + margin: 24px 0 12px 0; + color: #313236; } h3 { - margin-top: 35px; - font-size: 1.5em; + margin: 24px 0 12px 0; + font-size: 1.25em; } h4 { - margin-top: 30px; - font-size: 1.2em; + margin: 24px 0 12px 0; + font-size: 1.1em; + } + + h5 { + margin: 24px 0 12px 0; + font-size: 1em; + } + + h6 { + margin: 24px 0 12px 0; + font-size: 0.90em; } blockquote { - color: #888; + padding: 8px 21px; + margin: 12px 0 12px; + border-left: 3px solid #e7e9ed; + } + + blockquote p { + color: #7f8fa4 !important; font-size: 15px; line-height: 1.5; } - + + p { + color:#5c5d5e; + margin:6px 0 0 0; + } + table { @extend .table; @extend .table-bordered; + margin: 12px 0 12px 0; + color: #5c5d5e; th { - background: #EEE; - } + background: #f8fafc; + } + } + + pre { + @include border-radius(2px); + + margin: 12px 0 12px 0 !important; + background-color: #f8fafc !important; + font-size: 13px !important; + color: #5b6169 !important; + line-height: 1.6em !important; } p > code { - font-size: inherit; font-weight: inherit; } + + ul { + color: #5c5d5e; + } + li { - line-height: 1.5; + line-height: 1.6em; } a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { @@ -152,6 +195,7 @@ } } + @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; @@ -183,7 +227,7 @@ &.active { background: #f9f9f9; a { - font-weight: bold; + font-weight: 600; } } @@ -199,6 +243,11 @@ } } +.pull-right .light .fa-pencil { + top: 20px; + position: relative; +} + @mixin input-big { height: 36px; padding: 5px 10px; @@ -250,4 +299,4 @@ color: #78a; } } -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 2fc7bf1720..f6bdea9a89 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -12,8 +12,8 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #E7E9ED; -$background-color: #F8FAFC; +$border-color: #dce0e6; +$background-color: #F7F8FA; $header-height: 58px; $fixed-layout-width: 1200px; $gl-gray: #7f8fa4; diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 221cb6a04a..36e582d485 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -28,6 +28,7 @@ &.s48 { width: 48px; height: 48px; margin-right: 10px; } &.s60 { width: 60px; height: 60px; margin-right: 12px; } &.s90 { width: 90px; height: 90px; margin-right: 15px; } + &.s110 { width: 110px; height: 110px; margin-right: 15px; } &.s140 { width: 140px; height: 140px; margin-right: 20px; } &.s160 { width: 160px; height: 160px; margin-right: 20px; } } @@ -42,6 +43,7 @@ &.s32 { font-size: 22px; line-height: 32px; } &.s60 { font-size: 32px; line-height: 60px; } &.s90 { font-size: 36px; line-height: 90px; } + &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 140px; } &.s160 { font-size: 96px; line-height: 160px; } } diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index d5f0d86a30..bfb559c294 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -2,11 +2,17 @@ * Headers * */ + + body { + text-rendering: optimizeLegibility + } + .page-title { margin-top: 0px; - line-height: 1.5; - font-weight: normal; - margin-bottom: 5px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; + margin: 12px 7px 12px 7px; } h1, h2, h3, h4, h5, h6 { @@ -55,6 +61,7 @@ a > code { @include md-typography; word-wrap: break-word; + padding: 7px; /* Link to current header. */ h1, h2, h3, h4, h5, h6 { @@ -83,9 +90,12 @@ a > code { } } - ul { + ul,ol { padding: 0; - margin: 0 0 9px 25px !important; + margin: 6px 0 6px 18px !important; + } + ol { + color: #5c5d5e; } } @@ -111,4 +121,4 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a986fafff0..621ba2fe2c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -19,10 +19,10 @@ background: #f7f8fa; margin: -$gl-padding; padding: $gl-padding; - padding-top: 40px; + padding: 44px 0 17px 0; .project-identicon-holder { - margin-bottom: 15px; + margin-bottom: 16px; .avatar, .identicon { margin: 0 auto; @@ -40,23 +40,26 @@ .project-home-desc { h1 { + color: #313236; margin: 0; - margin-bottom: 10px; + margin-bottom: 6px; font-size: 23px; font-weight: normal; } p { - color: #7f8fa4; + color: #5c5d5e; } } .git-clone-holder { - max-width: 600px; - margin: 20px auto; + max-width: 498px; .form-control { background: #FFF; + font-size: 14px; + height: 42px; + margin-left: -2px; } } @@ -66,30 +69,66 @@ color: inherit; } } - + .input-group { + display: inline-table; + position: relative; + top: 17px; + margin-bottom: 44px; + } .project-repo-buttons { - margin-top: $gl-padding; - margin-bottom: 25px; + margin-top: 12px; + margin-bottom: 0px; .btn { @extend .btn-info; - + @include border-radius(2px); + + background-color: #f0f2f5; + border: 1px solid #dce0e5; text-transform: uppercase; - font-size: 15px; - line-height: 20px; - padding: 8px 14px; - border-radius: 3px; - margin-left: 10px; + color: #313236; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 16px; + margin-left: 12px; + + &:hover { + @include border-radius(2px); + color: #313236; + border: 1px solid #dce0e5; + background-color: #ebeef2; + } + + &:focus { + @include border-radius(2px); + color: #313236; + border: 1px solid #dce0e5; + background-color: #ebeef2; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + color: #313236 !important; + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; + } .count { - padding-left: 7px; display: inline-block; - margin-left: 7px; } } } } +.split-one { + display: inline-table; + + a { + margin: -1px !important; + } +} + .git-clone-holder { .project-home-dropdown + & { margin-right: 45px; @@ -103,7 +142,7 @@ } .input-group-addon { - background: #FAFAFA; + background: #f7f8fa; &.git-protocols { padding: 0; @@ -111,11 +150,157 @@ .input-group-btn:last-child > .btn { @include border-radius-right(0); + + border-left: 1px solid #c6cacf; + margin-left: -2px !important; + } + input-group-btn:first-child > .btn { + @include border-radius-left(0); } } } } +.projects-search-form { + + .input-group .btn-success { + background-color: #2ebf6b !important; + border: 1px solid #28b061 !important; + color: #fff !important; + + &:hover { + background-color: #2eb867 !important; + } + + &:focus { + background-color: #2eb867 !important; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: #28b061 !important; + border: 1px solid #26a65c !important; + color: #fff !important; + } + } + + .input-group .form-control { + height: 39px !important; + } + +} + +.input-group-btn > .btn { + background-color: #f0f2f5; + border: 1px solid #dce0e5; + text-transform: uppercase; + color: #313236; + font-size: 13px; + font-weight: 600; + + &:focus { + outline: none; + color: #313236; + border: 1px solid #dce0e5; + background-color: #ebeef2; + } + + &:hover { + outline: none; + color: #313236; + border: 1px solid #dce0e5; + background-color: #ebeef2; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; + } + +} + +.input-group-btn > .btn.active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + color: #313236 !important; + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; +} + +.split-repo-buttons { + display: inline-table; + margin-left: 12px; + + .btn { + margin: 0 !important; + } + + .dropdown { + margin: 0 !important; + } + + .dropdown-toggle { + margin: -5px !important; + } +} + +#notification-form { + margin-left: 5px; +} + +.open > .dropdown-toggle.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; + color: #313236; + font-size: 13px; + font-weight: 600; + color: #313236 !important; +} + +.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; + } + } + + .fa-fw { + 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; @@ -232,15 +417,48 @@ table.table.protected-branches-list tr.no-border { .project-stats { text-align: center; - margin-top: 0; + margin-top: 15px; margin-bottom: 0; - padding-top: 5px; - padding-bottom: 0; + padding-top: 10px; + padding-bottom: 4px; ul.nav-pills { display:inline-block; } + + .nav-pills li { + display:inline; + } + .nav > li > a { + border: 1px solid transparent; + color: #313236; + font-size: 13px; + font-weight: 600; + text-decoration: none; + text-transform: uppercase; + margin: 0 8px 0 0; + padding: 10px 16px 10px 16px; + + &:hover { + @include border-radius(2px); + border: 1px solid #dce0e5; + background-color: #f0f2f5; + } + + &:focus { + @include border-radius(2px); + border: 1px solid #dce0e5; + background-color: #f0f2f5; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border: 1px solid #c6cacf; + background-color: #e4e7ed; + } + } + li { display:inline; } @@ -251,11 +469,11 @@ table.table.protected-branches-list tr.no-border { } li.missing a { - color: #bbb; - border: 1px dashed #ccc; + color: #5a6069; + border: 1px dashed #dce0e5; &:hover { - background-color: #FAFAFA; + background-color: #f0f2f5; } } } @@ -273,6 +491,36 @@ pre.light-well { border-bottom: 1px solid #e7e9ed; } +.git-empty { + margin: 0 7px 0 7px; + + h5 { + color: #5c5d5e; + } + + .light-well { + @include border-radius (2px); + + color: #5b6169 !important; + font-size: 13px !important; + line-height: 1.6em !important; + } +} + +.prepend-top-20 { + margin: 20px 8px 20px 8px; + + .btn-remove { + @include border-radius (2px); + + font-size: 13px; + font-weight: 600px; + text-transform: uppercase; + float: left !important; + margin-bottom: 14px; + } +} + /* * Projects list rendered on dashboard and user page */ diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 6e53f55b0a..8c0980369f 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -16,18 +16,19 @@ .project-repo-buttons - = render 'projects/buttons/star' - - - unless empty_repo - = render 'projects/buttons/fork' - - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - = icon('download fw') - Download + .split-one + = render 'projects/buttons/star' + - unless empty_repo + = render 'projects/buttons/fork' + + = render "shared/clone_panel" + .split-repo-buttons + - unless empty_repo + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = icon('download fw') + + = render 'projects/buttons/dropdown' = render 'projects/buttons/notifications' - - = render 'projects/buttons/dropdown' - - = render "shared/clone_panel" + diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 854c154824..8f2f631eb7 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -8,6 +8,5 @@ - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do = icon('code-fork fw') - Fork %span.count = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 5d7df5ae09..3501dddefb 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,10 +1,6 @@ - if current_user = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do = icon('star fw') - - if current_user.starred?(@project) - Unstar - - else - Star %span.count = @project.star_count @@ -17,6 +13,5 @@ - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do = icon('star fw') - Star %span.count = @project.star_count diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 2cd422e772..b23b2f0d5e 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -24,4 +24,4 @@ .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } = visibility_level_icon(project.visibility_level) - = visibility_level_label(project.visibility_level).downcase + From 7d2655aae78604f8cc09e961868d520b23c102ce Mon Sep 17 00:00:00 2001 From: Ben Rosser Date: Thu, 24 Sep 2015 13:52:20 -0400 Subject: [PATCH 036/543] Remove 'kerberos' from auth_helper.rb for gitlab-CE. There is no Kerberos auth in gitlab-ce, so it shouldn't be noted as a form-driven auth mechanism in app/helpers/auth_helper.rb. This breaks using Kerberos as a custom omniauth provider. See issue #2510 --- app/helpers/auth_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index ce7e9b1db8..cd99a23240 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,6 +1,6 @@ module AuthHelper PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze - FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? Gitlab.config.ldap.enabled From 1868c8b25842cf24c79b8abf62ce5c177e5b82e3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 25 Sep 2015 10:04:09 +0200 Subject: [PATCH 037/543] Fix tests Signed-off-by: Dmitriy Zaporozhets --- features/steps/project/fork.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 370960845c..b0230add34 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -5,8 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I click link "Fork"' do expect(page).to have_content "Shop" - expect(page).to have_content "Fork" - click_link "Fork" + click_link "Fork project" end step 'I am a member of project "Shop"' do From 255d5f7501b01f2e88d614bf38b362a31d6cddc6 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 14:38:42 +0200 Subject: [PATCH 038/543] Improve 'rake gitlab:cleanup:repos' documentation --- doc/raketasks/cleanup.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 96d67f7b5d..8fbcbb983e 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -12,7 +12,8 @@ sudo gitlab-rake gitlab:cleanup:dirs bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production ``` -Remove repositories (global only for now) from `/home/git/repositories` if they don't exist in GitLab database. +Rename repositories from `/home/git/repositories` if they don't exist in GitLab database. +The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created. ``` # omnibus-gitlab From 6055c507cc05c02333fb986e5ec4b4d913c9e2d1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 25 Sep 2015 15:40:44 +0200 Subject: [PATCH 039/543] Fix path to /etc/default/gitlab in 8.0 update guide --- doc/update/7.14-to-8.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 86c0825dff..0cb82fb718 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -101,7 +101,7 @@ 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/gitlab/default`. +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 From 352f242d8ca491172db49d458f3eae31c70270c8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 25 Sep 2015 17:46:03 +0200 Subject: [PATCH 040/543] Fix admin runner page -> assign all button Signed-off-by: Dmitriy Zaporozhets --- app/models/ci/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index a52e28615f..8fb54b90d6 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -100,7 +100,7 @@ module Ci def unassigned(runner) joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). - where('#{Ci::RunnerProject.table_name}.project_id' => nil) + where("#{Ci::RunnerProject.table_name}.project_id" => nil) end def ordered_by_last_commit_date From b328c76c063cfe3082316e4273cfd318bbbfe69b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 25 Sep 2015 18:03:41 +0200 Subject: [PATCH 041/543] Move runners page to project settings Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/projects_controller.rb | 3 +- .../ci/runner_projects_controller.rb | 8 +- app/controllers/ci/runners_controller.rb | 73 ------------------- .../projects/runners_controller.rb | 69 ++++++++++++++++++ app/helpers/gitlab_routing_helper.rb | 8 ++ app/views/layouts/ci/_nav_project.html.haml | 5 -- .../layouts/nav/_project_settings.html.haml | 6 ++ .../runners/_runner.html.haml | 13 ++-- .../runners/_shared_runners.html.haml | 8 +- .../runners/_specific_runners.html.haml | 6 +- .../{ci => projects}/runners/edit.html.haml | 2 +- .../{ci => projects}/runners/index.html.haml | 0 .../{ci => projects}/runners/show.html.haml | 0 config/routes.rb | 15 ++-- 14 files changed, 111 insertions(+), 105 deletions(-) delete mode 100644 app/controllers/ci/runners_controller.rb create mode 100644 app/controllers/projects/runners_controller.rb rename app/views/{ci => projects}/runners/_runner.html.haml (54%) rename app/views/{ci => projects}/runners/_shared_runners.html.haml (68%) rename app/views/{ci => projects}/runners/_specific_runners.html.haml (81%) rename app/views/{ci => projects}/runners/edit.html.haml (90%) rename app/views/{ci => projects}/runners/index.html.haml (100%) rename app/views/{ci => projects}/runners/show.html.haml (100%) diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 6111753a7f..13766fb8f6 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -55,7 +55,8 @@ module Ci def toggle_shared_runners project.toggle!(:shared_runners_enabled) - redirect_to :back + + redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project) end def dumped_yaml diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb index a8bdd5bb36..97f01d40af 100644 --- a/app/controllers/ci/runner_projects_controller.rb +++ b/app/controllers/ci/runner_projects_controller.rb @@ -11,10 +11,12 @@ module Ci return head(403) unless current_user.ci_authorized_runners.include?(@runner) + path = runners_path(@project.gl_project) + if @runner.assign_to(project, current_user) - redirect_to ci_project_runners_path(project) + redirect_to path else - redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project' + redirect_to path, alert: 'Failed adding runner to project' end end @@ -22,7 +24,7 @@ module Ci runner_project = project.runner_projects.find(params[:id]) runner_project.destroy - redirect_to ci_project_runners_path(project) + redirect_to runners_path(@project.gl_project) end private diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb deleted file mode 100644 index a672370302..0000000000 --- a/app/controllers/ci/runners_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Ci - class RunnersController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] - before_action :authorize_access_project! - before_action :authorize_manage_project! - - layout 'ci/project' - - def index - @runners = @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) - @shared_runners = Ci::Runner.shared.active - @shared_runners_count = @shared_runners.count(:all) - end - - def edit - end - - def update - if @runner.update_attributes(runner_params) - redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.' - else - redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.' - end - end - - def destroy - if @runner.only_for?(@project) - @runner.destroy - end - - redirect_to ci_project_runners_path(@project) - end - - def resume - if @runner.update_attributes(active: true) - redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' - else - redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' - end - end - - def pause - if @runner.update_attributes(active: false) - redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' - else - redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' - end - end - - def show - end - - protected - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def set_runner - @runner ||= @project.runners.find(params[:id]) - end - - def runner_params - params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) - end - end -end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb new file mode 100644 index 0000000000..d59884a1dd --- /dev/null +++ b/app/controllers/projects/runners_controller.rb @@ -0,0 +1,69 @@ +class Projects::RunnersController < Projects::ApplicationController + before_action :ci_project + before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_action :authorize_admin_project! + + 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) + @shared_runners = Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + end + + def edit + end + + def update + if @runner.update_attributes(runner_params) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def destroy + if @runner.only_for?(@ci_project) + @runner.destroy + end + + redirect_to runners_path(@project) + end + + def resume + if @runner.update_attributes(active: true) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def show + end + + protected + + def ci_project + @ci_project = @project.gitlab_ci_project + end + + def set_runner + @runner ||= @ci_project.runners.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index e0816f4e71..4d9da6ff83 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -33,6 +33,14 @@ module GitlabRoutingHelper edit_namespace_project_path(project.namespace, project, *args) end + def runners_path(project, *args) + namespace_project_runners_path(project.namespace, project, *args) + end + + def runner_path(runner, *args) + namespace_project_runner_path(@project.namespace, @project, runner, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 7daf9342e4..9ebe7eabd8 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -11,11 +11,6 @@ %span Commits %span.count= @project.commits.count - = nav_link path: ['runners#index', 'runners#show', 'runners#edit'] do - = link_to ci_project_runners_path(@project) do - = icon('cog fw') - %span - Runners = nav_link path: 'variables#show' do = link_to ci_project_variables_path(@project) do = icon('code fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 857fb19995..a85dd71126 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -34,3 +34,9 @@ %span Protected Branches + - if @project.gitlab_ci? + = nav_link(controller: :runners) do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do + = icon('cog fw') + %span + Runners diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml similarity index 54% rename from app/views/ci/runners/_runner.html.haml rename to app/views/projects/runners/_runner.html.haml index ef8622e280..e6b8a2e6fe 100644 --- a/app/views/ci/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -3,9 +3,9 @@ = runner_status_icon(runner) %span.monospace - if @runners.include?(runner) - = link_to runner.short_sha, ci_project_runner_path(@project, runner) + = link_to runner.short_sha, runner_path(runner) %small - =link_to edit_ci_project_runner_path(@project, runner) do + =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn - else = runner.short_sha @@ -13,12 +13,12 @@ .pull-right - if @runners.include?(runner) - if runner.belongs_to_one_project? - = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else - - runner_project = @project.runner_projects.find_by(runner_id: runner) - = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - runner_project = @ci_project.runner_projects.find_by(runner_id: runner) + = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.specific? - = form_for [:ci, @project, @project.runner_projects.new] do |f| + = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' .pull-right @@ -32,4 +32,3 @@ - runner.tag_list.each do |tag| %span.label.label-primary = tag - diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml similarity index 68% rename from app/views/ci/runners/_shared_runners.html.haml rename to app/views/projects/runners/_shared_runners.html.haml index 944b3fd930..316ea747b1 100644 --- a/app/views/ci/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -3,11 +3,11 @@ .bs-callout.bs-callout-warning GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. %hr - - if @project.shared_runners_enabled - = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do + - if @ci_project.shared_runners_enabled + = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do Disable shared runners - else - = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do + = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do Enable shared runners   for this project @@ -17,7 +17,7 @@ - else %h4.underlined-title Available shared runners - #{@shared_runners_count} %ul.bordered-list.available-shared-runners - = render @shared_runners.first(10) + = render partial: 'runner', collection: @shared_runners, as: :runner - if @shared_runners_count > 10 .light and #{@shared_runners_count - 10} more... diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml similarity index 81% rename from app/views/ci/runners/_specific_runners.html.haml rename to app/views/projects/runners/_specific_runners.html.haml index 0604e7a46c..c13625c7e4 100644 --- a/app/views/ci/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -12,7 +12,7 @@ %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: - %code #{@project.token} + %code #{@ci_project.token} %li Start runner! @@ -20,10 +20,10 @@ - if @runners.any? %h4.underlined-title Runners activated for this project %ul.bordered-list.activated-specific-runners - = render @runners + = render partial: 'runner', collection: @runners, as: :runner - if @specific_runners.any? %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners - = render @specific_runners + = render partial: 'runner', collection: @specific_runners, as: :runner = paginate @specific_runners diff --git a/app/views/ci/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml similarity index 90% rename from app/views/ci/runners/edit.html.haml rename to app/views/projects/runners/edit.html.haml index 81c8e58ae2..66851d3831 100644 --- a/app/views/ci/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,6 +1,6 @@ %h4 Runner ##{@runner.id} %hr -= form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f| += form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| .form-group = label :active, "Active", class: 'control-label' .col-sm-10 diff --git a/app/views/ci/runners/index.html.haml b/app/views/projects/runners/index.html.haml similarity index 100% rename from app/views/ci/runners/index.html.haml rename to app/views/projects/runners/index.html.haml diff --git a/app/views/ci/runners/show.html.haml b/app/views/projects/runners/show.html.haml similarity index 100% rename from app/views/ci/runners/show.html.haml rename to app/views/projects/runners/show.html.haml diff --git a/config/routes.rb b/config/routes.rb index 4a07c449b4..a81ed1b95f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,13 +55,6 @@ Gitlab::Application.routes.draw do resources :triggers, only: [:index, :create, :destroy] - resources :runners, only: [:index, :edit, :update, :destroy, :show] do - member do - get :resume - get :pause - end - end - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -652,8 +645,14 @@ Gitlab::Application.routes.draw do get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } end end - end + resources :runners, only: [:index, :edit, :update, :destroy, :show] do + member do + get :resume + get :pause + end + end + end end end From 253d2320ab3d3473509d6abe4a62be90428f20c4 Mon Sep 17 00:00:00 2001 From: Paul Beattie Date: Thu, 10 Sep 2015 16:57:43 +0100 Subject: [PATCH 042/543] Add support for AWS S3 Server-Side Encryption support This adds support for AWS S3 SSE with S3 managed keys, this means the data is encrypted at rest and the encryption is handled transparently to the end user as well as in the AWS Console. This is optional and not required to make S3 uploads work. --- CHANGELOG | 1 + config/gitlab.yml.example | 30 ++++++++++++++++-------------- config/initializers/1_settings.rb | 1 + doc/raketasks/backup_restore.md | 2 ++ lib/backup/manager.rb | 7 ++++--- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8ff61cd6e9..4172d10c8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.0.2 (unreleased) - Use standard Markdown font in Markdown preview instead of fixed-width font (Stan Hu) - Fix Reply by email for non-UTF-8 messages. - Add option to use StartTLS with Reply by email IMAP server. + - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie) v 8.0.1 - Remove git refs used internally by GitLab from network graph (Stan Hu) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 15930fc907..c7174f8601 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -159,7 +159,7 @@ production: &base method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' - + # This setting specifies if LDAP server is Active Directory LDAP server. # For non AD servers it skips the AD specific queries. # If your LDAP server is not AD, set this to false. @@ -204,13 +204,13 @@ production: &base # The username will be used in paths for the user's own projects # (like `gitlab.example.com/username/project`) and when mentioning # them in issues, merge request and comments (like `@username`). - # If the attribute specified for `username` contains an email address, + # If the attribute specified for `username` contains an email address, # the GitLab username will be the part of the email address before the '@'. username: ['uid', 'userid', 'sAMAccountName'] email: ['mail', 'email', 'userPrincipalName'] # If no full name could be found at the attribute specified for `name`, - # the full name is determined using the attributes specified for + # the full name is determined using the attributes specified for # `first_name` and `last_name`. name: 'cn' first_name: 'givenName' @@ -252,28 +252,28 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', + # - { name: 'google_oauth2', # label: 'Google', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', - # app_id: 'YOUR_APP_ID', + # - { name: 'twitter', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET' } - # - { name: 'github', + # - { name: 'github', # label: 'GitHub', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } - # - { name: 'gitlab', + # - { name: 'gitlab', # label: 'GitLab.com', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } - # - { name: 'bitbucket', - # app_id: 'YOUR_APP_ID', + # - { name: 'bitbucket', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET' } - # - { name: 'saml', + # - { name: 'saml', # label: 'Our SAML Provider', # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', @@ -319,6 +319,8 @@ production: &base # # Use multipart uploads when file size reaches 100MB, see # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html # multipart_chunk_size: 104857600 + # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional + # # encryption: 'AES256' ## GitLab Shell settings gitlab_shell: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 48601b6733..4e4a8ecbdb 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -229,6 +229,7 @@ if Settings.backup['upload']['connection'] Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] end Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 +Settings.backup['upload']['encryption'] ||= nil # # Git diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 4ff5e74d43..b212964436 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -95,6 +95,8 @@ For installations from source: aws_secret_access_key: 'secret123' # The remote 'directory' to store your backups. For S3, this would be the bucket name. remote_directory: 'my.s3.bucket' + # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional + # encryption: 'AES256' ``` If you are uploading your backups to S3 you will probably want to create a new diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ac63f89c6e..5c42f25f4a 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -45,7 +45,8 @@ module Backup directory = connection.directories.get(remote_directory) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, - multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size) + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, + encryption: Gitlab.config.backup.upload.encryption) $progress.puts "done".green else puts "uploading backup to #{remote_directory} failed".red @@ -55,7 +56,7 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - + backup_contents.each do |dir| next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) @@ -75,7 +76,7 @@ module Backup if keep_time > 0 removed = 0 - + Dir.chdir(Gitlab.config.backup.path) do file_list = Dir.glob('*_gitlab_backup.tar') file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } From 8a1bc6040b5781620888da2892093c107bb90fb8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 25 Sep 2015 14:05:25 -0400 Subject: [PATCH 043/543] Add unreleased 8.0.3 entry to CHANGELOG [ci skip] --- CHANGELOG | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8ff61cd6e9..b495ccc9b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,7 +13,9 @@ v 8.1.0 (unreleased) - Remove "Continuous Integration" page from dashboard - Add notes and SSL verification entries to hook APIs (Ben Boeckel) -v 8.0.2 (unreleased) +v 8.0.3 (unreleased) + +v 8.0.2 - Fix default avatar not rendering in network graph (Stan Hu) - Skip check_initd_configured_correctly on omnibus installs - Prevent double-prefixing of help page paths @@ -21,6 +23,7 @@ v 8.0.2 (unreleased) - Make commit graphs responsive to window width changes (Stan Hu) - Fix top margin for sign-in button on public pages - Fix LDAP attribute mapping + - Remove git refs used internally by GitLab from network graph (Stan Hu) - Use standard Markdown font in Markdown preview instead of fixed-width font (Stan Hu) - Fix Reply by email for non-UTF-8 messages. - Add option to use StartTLS with Reply by email IMAP server. From 8f1da76f409c53fcf3db8d83be97bebdc6ed4998 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 25 Sep 2015 14:05:51 -0400 Subject: [PATCH 044/543] Remove CHANGELOG-CI [ci skip] --- CHANGELOG-CI | 298 --------------------------------------------------- 1 file changed, 298 deletions(-) delete mode 100644 CHANGELOG-CI diff --git a/CHANGELOG-CI b/CHANGELOG-CI deleted file mode 100644 index d1ad661d88..0000000000 --- a/CHANGELOG-CI +++ /dev/null @@ -1,298 +0,0 @@ -v7.14.0 (unreleased) - - Truncate commit messages after subject line in table - - Adjust CI config to support Docker executors - - Added Application Settings - - Randomize test database for CI tests - - Make YAML validation stricter - - Use avatars received from GitLab - - Refactor GitLab API usage to use either access_token or private_token depending on what was specified during login - - Allow to use access_token for API requests - - Fix project API listing returning empty list when first projects are not added to CI - - Allow to define variables from YAML - - Added support for CI skipped status - - Fix broken yaml error saving - - Add committed_at to commits to properly order last commit (the force push issue) - - Rename type(s) to stage(s) - - Fix navigation icons - - Add missing stage when doing retry - - Require variable keys to be not-empty and unique - - Fix variable saving issue - - Display variable saving errors in variables page not the project's - - Added Build Triggers API - -v7.13.1 - - Fix: user could steal specific runner - - Fix: don't send notifications for jobs with allow_failure set - - Fix invalid link to doc.gitlab.com - -v7.13.0 - - Fix inline edit runner-description - - Allow to specify image and services in yml that can be used with docker - - Fix: No runner notification can see managers only - - Fix service testing for slack - - Ability to cancel all builds in commit at once - - Disable colors in rake tasks automatically (if IO is not a TTY) - - Implemented "rake env:info". Rake task to receive system information - - Fix coverage calculation on commit page - - Enhance YAML validation - - Redirect back after authorization - - Change favicon - - Refactoring: Get rid of private_token usage in the frontend. - - Allow to specify allow_failure for job - - Build traces is stored in the file instead of database - - Make the builds path configurable - - Disable link to runner if it's not assigned to specific project - - Store all secrets in config/secrets.yml - - Encrypt variables - - Allow to specify flexible list of types in yaml - -v7.12.2 - - Revert: Runner without tag should pick builds without tag only - -v7.12.1 - - Runner without tag should pick builds without tag only - - Explicit error in the GitLab when commit not found. - - Fix: lint with relative subpath - - Update webhook example - - Improved Lint stability - - Add warning when .gitlab-ci.yml not found - - Improved validation for .gitlab-ci.yml - - Fix list of branches in only section - - Fix "Status Badge" button - -v7.12.0 - - Endless scroll on the dashboard - - Add notification if there are no runners - - Fix pagination on dashboard - - Remove ID column from runners list in the admin area - - Increase default timeout for builds to 60 minutes - - Using .gitlab-ci.yml file instead of jobs - - Link to the runner from the build page for admin user - - Ability to set secret variables for runner - - Dont retry build when push same commit in same ref twice - - Admin area: show amount of runners with last contact less than a minute ago - - Fix re-adding project with the same name but different gitlab_id - - Implementation of Lint (.gitlab-ci.yml validation tool) - - Updated rails to 4.1.11 - - API fix: project create call - - Link to web-editor with .gitlab-ci.yml - - Updated examples in the documentation - -v7.11.0 - - Deploy Jobs API calls - - Projects search on dashboard page - - Improved runners page - - Running and Pending tabs on admin builds page - - Fix [ci skip] tag, so you can skip CI triggering now - - Add HipChat notifications - - Clean up project advanced settings. - - Add a GitLab project path parameter to the project API - - Remove projects IDs from dashboard - - UI fix: Remove page headers from the admin area - - Improve Email templates - - Add backup/restore utility - - Coordinator stores information(version, platform, revision, etc.) about runners. - - Fixed pagination on dashboard - - Public accessible build and commit pages of public projects - - Fix vulnerability in the API when MySQL is used - -v7.10.1 - - Fix failing migration when update to 7.10 from 7.8 and older versions - -sidekiq_wirker_fix - - added sidekiq.yml - - integrated in script/background_jobs -v7.10.0 - - Projects sorting by last commit date - - Add project search at runner page - - Fix GitLab and CI projects collision - - Events for admin - - Events per projects - - Search for runners in admin area - - UI improvements: created separated admin section, removed useless project show page - - Runners sorting in admin area (by id) - - Remove protected_attributes gem - - Skip commit creation if there is no appropriate job - -v7.9.3 - - Contains no changes - - Developers can cancel and retry jobs - -v7.9.2 - - [Security] Already existing projects should not be served by shared runners - - Ability to run deploy job without test jobs (every push will trigger deploy job) - -v7.9.1 - - [Security] Adding explicit is_shared parameter to runner - - [Security] By default new projects are not served by shared runners - -v7.9.0 - - Reset user session if token is invalid - - Runner delete api endpoint - - Fix bug about showing edit button on commit page if user does not have permissions - - Allow to pass description and tag list during Runner's registration - - Added api for project jobs - - Implementation of deploy jobs after all parallel jobs(tests). - - Add scroll up/down buttons for better mobile experience with large build traces - - Add runner last contact (Kamil TrzciÅ„ski) - - Allow to pause runners - when paused runner will not receive any new build (Kamil TrzciÅ„ski) - - Add brakeman (security scanner for Ruby on Rails) - - Changed a color of the canceled builds - - Fix of show the same commits in different branches - -v7.8.2 - - Fix the broken build failed email - - Notify only pusher instead of commiter - -v7.8.0 - - Fix OAuth login with GitLab installed in relative URL - - GitLab CI has same version as GitLab since now - - Allow to pass description and tag list during Runner's registration (Kamil TrzciÅ„ski) - - Update documentation (API, Install, Update) - - Skip refs field supports for wildcard branch name (ex. feature/*) - - Migrate E-mail notification to Services menu (Kamil TrzciÅ„ski) - - Added Slack notifications (Kamil TrzciÅ„ski) - - Disable turbolink on links pointing out to GitLab server - - Add test coverage parsing example for pytest-cov - - Upgrade raindrops gem - -v5.4.2 - - Fix exposure of project token via build data - -v5.4.1 - - Fix 500 if on builds page if build has no job - - Truncate project token from build trace - - Allow users with access to project see build trace - -v5.4.0 (Requires GitLab 7.7) - - Fixed 500 error for badge if build is pending - - Non-admin users can now register specific runners for their projects - - Project specific runners page which users can access - - Remove progress output from schedule_builds cron job - - Fix schedule_builds rake task - - Fix test webhook button - - Job can be branch specific or tag specific or both - - Shared runners builds projects which are not assigned to specific ones - - Job can be runner specific through tags - - Runner have tags - - Move job settings to separate page - - Add authorization level managing projects - - OAuth authentication via GitLab. - -v5.3 - - Remove annoying 'Done' message from schedule_builds cron job - - Fix a style issue with the navbar - - Skip CSRF check on the project's build page - - Fix showing wrong build script on admin projects page - - Add branch and commit message to build result emails - -v5.2 - - Improve performance by adding new indicies - - Separate Commit logic from Build logic in prep for Parallel Builds - - Parallel builds - - You can have multiple build scripts per project - -v5.1 - - Registration token and runner token are named differently - - Redirect to previous page after sign-in - - Dont show archived projects - - Add support for skip branches from build - - Add coverage parsing feature - - Update rails to 4.0.10 - - Look for a REVISION file before running `git log` - - All builds page for admin - -v5.0.1 - - Update rails to 4.0.5 - -v5.0.0 - - Set build timeout in minutes - - Web Hooks for builds - - Nprogress bar - - Remove extra spaces in build script - - Requires runner v5 - * All script commands executed as one file - * Cancel button works correctly now - * Runner stability increased - * Timeout applies to build now instead of line of script - -v4.3.0 - - Refactor build js - - Redirect to build page with sha + bid if build id is not provided - - Update rails to 4.0.3 - - Restyle project settings page - - Improve help page - - Replaced puma with unicorn - - Improved init.d script - - Add submodule init to default build script for new projects - -v4.2.0 - - Build duration chart - - Bootstrap 3 with responsive UI - - Improved init.d script - - Refactoring - - Changed http codes for POST /projects/:id/build action - - Turbolinks - -v4.1.0 - - Rails 4 - - Click on build branch to see other builds for this branch - - Email notifications (Jeroen Knoops) - -v4.0.0 - - Shared runners (no need to add runner to every project) - - Admin area (only available for GitLab admins) - - Hide all runners management into admin area - - Use http cloning for builds instead of deploy keys - - Allow choose between git clone and git fetch when get code for build - - Make build timeout actually works - - Requires GitLab 6.3 or higher - - GitLab CI settings go to GitLab project via api on creation - -v3.2.0 - - Limit visibility of projects by gitlab authorized projects - - Use one page for both gitlab and gitlab-ci projects - -v3.1.0 - - Login with both username, email or LDAP credentials (if GitLab 6.0+) - - Retry build button functionality - - UI fixes for resolution 1366px and lower - - Fix gravatar ssl warning - -v3.0.0 - - Build running functionality extracted in gitlab-ci-runner - - Added API for runners and builds - - Redesigned application - - Added charts - - Use GitLab auth - - Add projects via UI with few clicks - -v2.2.0 - - replaced unicorn with puma - - replaced grit with rugged - - Runner.rb more transactional safe now - - updated rails to 3.2.13 - - updated devise to 2.2 - - fixed issue when build left in running status if exception triggered - - rescue build timeout correctly - - badge helper with markdown & html - - increased test coverage to 85% - -v2.1.0 - - Removed horizontal scroll for build trace - - new status badges - - better encode - - added several CI_* env variables - -v2.0.0 - - Replace resque with sidekiq - - Run only one build at time per project - - Added whenever for schedule jobs - -v1.2.0 - - Added Github web hook support - - Added build schedule - -v1.1.0 - - Added JSON response for builds status - - Compatible with GitLab v4.0.0 \ No newline at end of file From 12acf15c90d25a22e706737dc54f17466fb30320 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 25 Sep 2015 20:33:05 +0200 Subject: [PATCH 045/543] Project page Update refactoring buttons, fixes for projects filter on the dashboard and group page --- app/assets/stylesheets/base/mixins.scss | 2 +- app/assets/stylesheets/generic/buttons.scss | 138 +++++++++++ app/assets/stylesheets/generic/header.scss | 1 - app/assets/stylesheets/generic/sidebar.scss | 3 +- .../stylesheets/generic/typography.scss | 8 +- app/assets/stylesheets/pages/groups.scss | 6 + app/assets/stylesheets/pages/projects.scss | 218 ++++++------------ .../dashboard/projects/_projects.html.haml | 2 +- app/views/groups/_projects.html.haml | 2 +- .../projects/buttons/_dropdown.html.haml | 2 +- .../projects/buttons/_notifications.html.haml | 2 +- app/views/projects/empty.html.haml | 74 +++--- 12 files changed, 263 insertions(+), 195 deletions(-) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index cdebe49891..5a7e79ddec 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -299,4 +299,4 @@ color: #78a; } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 46ef595ddf..cf76f538e0 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,3 +1,6 @@ +body { + text-rendering: geometricPrecision; +} .btn { @extend .btn-default; @@ -88,3 +91,138 @@ } } } + +@mixin btn-info { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 16px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + +@mixin btn-middle { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 24px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + + +@mixin btn-green { + background-color: #28b061; + border: 1px solid #26a65c; + color: #fff; + + &:hover { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:focus { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: #23a158 !important; + border: 1px solid #229954 !important; + color: #fff !important; + } +} + +/*Butons*/ + +@mixin bnt-project { + background-color: #f0f2f5; + border-color: #dce0e5; + color: #313236; + + &:hover { + border-color:#dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:focus { + border-color: #dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; + } +} + +@mixin btn-remove { + background-color: #f72e60; + border-color: #ee295a; + + &:hover { + background-color: #e82757; + border-color: #e32555; + } + + &:focus { + background-color: #e82757; + border-color: #e32555; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + background-color: #d42450 !important; + border-color: #e12554 !important; + } + +} \ No newline at end of file diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index b758a526fb..543ce41ab5 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -26,7 +26,6 @@ header { min-height: $header-height; background-color: #fff; border: none; - border-bottom: 1px solid #EEE; .container-fluid { width: 100% !important; diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index 3d055f0e66..c5ea3aca7c 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -21,12 +21,11 @@ min-height: 100vh; width: 100%; padding: 20px; - background: #f1f4f8; + background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; - border: 1px solid #e7e9ed; min-height: 90vh; &.container-blank { diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index bfb559c294..0ccb21f3cd 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -2,10 +2,10 @@ * Headers * */ - - body { - text-rendering: optimizeLegibility - } +body { + text-rendering:optimizeLegibility; + -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; +} .page-title { margin-top: 0px; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 2b1b747139..07a38a19fa 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -10,3 +10,9 @@ .milestone-row { @include str-truncated(90%); } + +.dashboard .side .panel .panel-heading .input-group { + .form-control { + height: 42px; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 621ba2fe2c..a5940543a9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1,3 +1,14 @@ +.alert_holder { + margin: -16px; + + .alert-link { + font-weight: normal; + } +} +.no-ssh-key-message { + background-color: #f28d35; + margin-bottom: 16px; +} .new_project, .edit_project { fieldset.features { @@ -20,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; } @@ -59,7 +70,7 @@ background: #FFF; font-size: 14px; height: 42px; - margin-left: -2px; + margin-left: -1px; } } @@ -75,45 +86,15 @@ top: 17px; margin-bottom: 44px; } + .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; - + .btn { - @extend .btn-info; - @include border-radius(2px); + @include bnt-project; + @include btn-info; - background-color: #f0f2f5; - border: 1px solid #dce0e5; - text-transform: uppercase; - color: #313236; - font-size: 13px; - font-weight: 600; - line-height: 18px; - padding: 11px 16px; - margin-left: 12px; - - &:hover { - @include border-radius(2px); - color: #313236; - border: 1px solid #dce0e5; - background-color: #ebeef2; - } - - &:focus { - @include border-radius(2px); - color: #313236; - border: 1px solid #dce0e5; - background-color: #ebeef2; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - color: #313236 !important; - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; - } - .count { display: inline-block; } @@ -123,6 +104,7 @@ .split-one { display: inline-table; + margin-right: 12px; a { margin: -1px !important; @@ -138,7 +120,7 @@ cursor: auto; @extend .monospace; background: #FAFAFA; - width: 100%; + width: 101%; } .input-group-addon { @@ -147,106 +129,66 @@ &.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; } - input-group-btn:first-child > .btn { - @include border-radius-left(0); - } } } } .projects-search-form { - .input-group .btn-success { - background-color: #2ebf6b !important; - border: 1px solid #28b061 !important; - color: #fff !important; - - &:hover { - background-color: #2eb867 !important; - } - - &:focus { - background-color: #2eb867 !important; - } - - &:active { - @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); - - background-color: #28b061 !important; - border: 1px solid #26a65c !important; - color: #fff !important; - } - } - .input-group .form-control { - height: 39px !important; + height: 42px; } - } -.input-group-btn > .btn { - background-color: #f0f2f5; - border: 1px solid #dce0e5; - text-transform: uppercase; - color: #313236; - font-size: 13px; - font-weight: 600; - - &:focus { - outline: none; - color: #313236; - border: 1px solid #dce0e5; - background-color: #ebeef2; +.input-group-btn { + .btn { + @include bnt-project; + @include btn-middle; + + &:hover { + outline: none; + } + + &:focus { + outline: none; + } + + &:active { + outline: none; + } } - &:hover { - outline: none; - color: #313236; - border: 1px solid #dce0e5; - background-color: #ebeef2; - } - - &:active { + .active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - color: #313236 !important; border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; } - -} - -.input-group-btn > .btn.active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; - color: #313236 !important; - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; + .btn-green { + @include btn-green + } + } .split-repo-buttons { display: inline-table; - margin-left: 12px; + margin: 0 12px 0 12px; - .btn { - margin: 0 !important; - } - - .dropdown { - margin: 0 !important; + .btn{ + @include bnt-project; + @include btn-info; } .dropdown-toggle { - margin: -5px !important; + margin: -5px; } } @@ -254,22 +196,25 @@ margin-left: 5px; } -.open > .dropdown-toggle.btn { +.dropdown-new { + margin-left: -5px; +} + +.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; - color: #313236; + color: #313236 !important; font-size: 13px; font-weight: 600; - color: #313236 !important; } .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; @@ -298,7 +243,7 @@ } .project-home-panel .project-home-dropdown { - margin: 13px 0px 0; + margin: 13px 0px 0; } .project-visibility-level-holder { @@ -431,32 +376,12 @@ table.table.protected-branches-list tr.no-border { } .nav > li > a { - border: 1px solid transparent; - color: #313236; - font-size: 13px; - font-weight: 600; - text-decoration: none; - text-transform: uppercase; - margin: 0 8px 0 0; - padding: 10px 16px 10px 16px; + @include btn-info; + @include bnt-project; - &:hover { - @include border-radius(2px); - border: 1px solid #dce0e5; - background-color: #f0f2f5; - } - - &:focus { - @include border-radius(2px); - border: 1px solid #dce0e5; - background-color: #f0f2f5; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border: 1px solid #c6cacf; - background-color: #e4e7ed; - } + background-color: transparent; + border: 1px solid #f7f8fa; + margin-left: 12px; } li { @@ -501,29 +426,27 @@ pre.light-well { .light-well { @include border-radius (2px); - color: #5b6169 !important; - font-size: 13px !important; - line-height: 1.6em !important; + color: #5b6169; + font-size: 13px; + line-height: 1.6em; } } .prepend-top-20 { - margin: 20px 8px 20px 8px; + margin-top: 20px; .btn-remove { - @include border-radius (2px); + @include btn-middle; + @include btn-remove; - font-size: 13px; - font-weight: 600px; - text-transform: uppercase; float: left !important; - margin-bottom: 14px; } } /* * Projects list rendered on dashboard and user page */ + .projects-list { @include basic-list; @@ -581,3 +504,4 @@ pre.light-well { .inline-form { display: inline-block; } + diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index ef9b9ce756..0afe4e651c 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -4,7 +4,7 @@ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? %span.input-group-btn - = link_to new_project_path, class: 'btn btn-success' do + = link_to new_project_path, class: 'btn btn-green' do New project = render 'shared/projects/list', projects: @projects diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 9ac56b1e5f..2b27a88794 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -4,7 +4,7 @@ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if can? current_user, :create_projects, @group %span.input-group-btn - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do New project = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index bc7625e898..4580c91269 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user %span.dropdown - %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 57f764178d..4b69a6d7a6 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-toggle.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') = notification_label(@membership) = icon('angle-down') diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 185ebf2393..d8f9f692c0 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,7 +1,8 @@ -- if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' - +.alert_holder + - if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render 'shared/no_password' + = render "home_panel" .gray-content-block.center @@ -15,38 +16,39 @@ file to this project. .prepend-top-20 -%h3.page-title - 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}" +.empty_wrapper + %h3.page-title + 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 3453b79c9d57d07d6772bdebd37ab691bfd84723 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 25 Sep 2015 15:43:20 -0400 Subject: [PATCH 046/543] Correct an inaccuracy in the Markdown doc [ci skip] --- doc/markdown/markdown.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index d83838127c..ac3851f8c9 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -274,7 +274,7 @@ The IDs are generated from the content of the header according to the following 1. All spaces are converted to hyphens 1. Two or more hyphens in a row are converted to one 1. If a header with the same ID has already been generated, a unique - incrementing number is appended. + incrementing number is appended, starting at 1. For example: @@ -291,8 +291,8 @@ Would generate the following link IDs: 1. `this-header-has-spaces-in-it` 1. `this-header-has-a-in-it` 1. `this-header-has-unicode-in-it-한글` +1. `this-header-has-spaces-in-it` 1. `this-header-has-spaces-in-it-1` -1. `this-header-has-spaces-in-it-2` Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. From 0383afc66ab889afc6af02203902d1d515723a96 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 25 Sep 2015 17:04:20 -0700 Subject: [PATCH 047/543] Add user preference to view project activity and starred project activity as default dashboard Closes #2662 --- CHANGELOG | 1 + app/controllers/root_controller.rb | 4 ++++ app/helpers/preferences_helper.rb | 4 +++- app/models/user.rb | 2 +- spec/controllers/root_controller_spec.rb | 24 +++++++++++++++++++++++- spec/helpers/preferences_helper_spec.rb | 8 ++++++-- 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b495ccc9b9..169812e823 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) + - Add user preference to view activities as default dashboard (Stan Hu) - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) - Show CI status on all pages where commits list is rendered diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 54171ff67c..ad04c646e1 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -22,6 +22,10 @@ class RootController < Dashboard::ProjectsController when 'stars' flash.keep redirect_to starred_dashboard_projects_path + when 'project_activity' + redirect_to activity_dashboard_path + when 'starred_project_activity' + redirect_to activity_dashboard_path(filter: 'starred') else return end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 7f1b6a6992..1b1f4162df 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -3,7 +3,9 @@ module PreferencesHelper # Maps `dashboard` values to more user-friendly option text DASHBOARD_CHOICES = { projects: 'Your Projects (default)', - stars: 'Starred Projects' + stars: 'Starred Projects', + project_activity: "Your Projects' Activity", + starred_project_activity: "Starred Projects' Activity" }.with_indifferent_access.freeze # Returns an Array usable by a select field for more user-friendly option text diff --git a/app/models/user.rb b/app/models/user.rb index 25371f9138..3879f3fd38 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -172,7 +172,7 @@ class User < ActiveRecord::Base # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars] + enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] # User's Project preference # Note: When adding an option, it MUST go on the end of the array. diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index 64dfe8f34e..5a104ae7c9 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -10,7 +10,7 @@ describe RootController do allow(subject).to receive(:current_user).and_return(user) end - context 'who has customized their dashboard setting' do + context 'who has customized their dashboard setting for starred projects' do before do user.update_attribute(:dashboard, 'stars') end @@ -21,6 +21,28 @@ describe RootController do end end + context 'who has customized their dashboard setting for project activities' do + before do + user.update_attribute(:dashboard, 'project_activity') + end + + it 'redirects to the activity list' do + get :index + expect(response).to redirect_to activity_dashboard_path + end + end + + context 'who has customized their dashboard setting for starred project activities' do + before do + user.update_attribute(:dashboard, 'starred_project_activity') + end + + it 'redirects to the activity list' do + get :index + expect(response).to redirect_to activity_dashboard_path(filter: 'starred') + end + end + context 'who uses the default dashboard setting' do it 'renders the default dashboard' do get :index diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 06f69262b7..e5df59c4fb 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -8,14 +8,18 @@ describe PreferencesHelper do end it 'raises an exception when defined choices may be using the wrong key' do - expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar') + dashboards = User.dashboards.dup + dashboards[:projects_changed] = dashboards.delete :projects + expect(User).to receive(:dashboards).and_return(dashboards) expect { helper.dashboard_choices }.to raise_error(KeyError) end it 'provides better option descriptions' do expect(helper.dashboard_choices).to match_array [ ['Your Projects (default)', 'projects'], - ['Starred Projects', 'stars'] + ['Starred Projects', 'stars'], + ["Your Projects' Activity", 'project_activity'], + ["Starred Projects' Activity", 'starred_project_activity'] ] end end From 922c635b6737bbf13eab11ae74c10e590321002c Mon Sep 17 00:00:00 2001 From: Cyriac Thomas Date: Sat, 26 Sep 2015 18:15:58 +0000 Subject: [PATCH 048/543] Fixed grammar on Using Docker Build doc. --- doc/ci/docker/using_docker_build.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index a698fbc818..caa6d0f1f5 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -1,6 +1,6 @@ # Using Docker Build -GitLab CI can allows you to use Docker Engine to build and test docker-based projects. +GitLab CI allows you to use Docker Engine to build and test docker-based projects. **This also allows to you to use `docker-compose` and other docker-enabled tools.** @@ -108,5 +108,4 @@ In order to do that follow the steps: ``` 1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout. -For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). - +For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). \ No newline at end of file From a9cfa6c50819a959db9f62efe57803f667c92967 Mon Sep 17 00:00:00 2001 From: Aaron Snyder <=> Date: Fri, 25 Sep 2015 21:14:16 -0700 Subject: [PATCH 049/543] Fix grammar in admin area labels .nothing-here-block when no labels exist. updating admin area > "Labels" text to "There are no labels yet.", per Stan Hus suggestion. --- CHANGELOG | 1 + app/views/admin/labels/index.html.haml | 4 ++-- features/steps/admin/labels.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8ff61cd6e9..9ea016e7cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.1.0 (unreleased) - 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) + - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. v 8.0.2 (unreleased) - Fix default avatar not rendering in network graph (Stan Hu) diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 8b11c28c56..d67454c03e 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -12,5 +12,5 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block There are no any labels yet - \ No newline at end of file + .nothing-here-block There are no labels yet + diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb index d64380abf7..b45d98658b 100644 --- a/features/steps/admin/labels.rb +++ b/features/steps/admin/labels.rb @@ -38,7 +38,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps step 'I should see labels help message' do page.within '.labels' do - expect(page).to have_content 'There are no any labels yet' + expect(page).to have_content 'There are no labels yet' end end From 05f29a5cb30d640455a2f4336599a145bc0d0af7 Mon Sep 17 00:00:00 2001 From: Cyriac Thomas Date: Sat, 26 Sep 2015 19:37:55 +0000 Subject: [PATCH 050/543] Simplified sentence as per @stanhu's note --- doc/ci/docker/using_docker_build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index caa6d0f1f5..5af27470d2 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -108,4 +108,4 @@ In order to do that follow the steps: ``` 1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout. -For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). \ No newline at end of file +For more information, check out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). \ No newline at end of file From 436ab6d3ed1b9a600f0061d5ff350dd01f81574d Mon Sep 17 00:00:00 2001 From: fscherwi Date: Sun, 27 Sep 2015 12:47:12 +0200 Subject: [PATCH 051/543] change build status image to svg [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99d5bc0b6c..91855b42d2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GitLab -[![build status](https://ci.gitlab.com/projects/1/status.png?ref=master)](https://ci.gitlab.com/projects/1?ref=master) +[![build status](https://ci.gitlab.com/projects/1/status.svg?ref=master)](https://ci.gitlab.com/projects/1?ref=master) [![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) From 92f8e0fd7e1d39fb14b5a1849971ad74abf1280d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Sep 2015 10:14:53 +0200 Subject: [PATCH 052/543] Finish move of runners page to project settings Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/ci/projects/show.html.haml | 2 +- app/views/ci/shared/_guide.html.haml | 2 +- spec/features/ci/runners_spec.rb | 96 ---------------------------- 4 files changed, 3 insertions(+), 98 deletions(-) delete mode 100644 spec/features/ci/runners_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 8ff61cd6e9..7c7e41db16 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.1.0 (unreleased) - 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) + - Move CI runners page to project settings area v 8.0.2 (unreleased) - Fix default avatar not rendering in network graph (Stan Hu) diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml index 6443378af9..73e60795ba 100644 --- a/app/views/ci/projects/show.html.haml +++ b/app/views/ci/projects/show.html.haml @@ -3,7 +3,7 @@ - if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners? .alert.alert-danger Builds for this project wont be served unless you configure runners on - = link_to "Runners page", ci_project_runners_path(@project) + = link_to "Runners page", runners_path(@project.gl_project) %ul.nav.nav-tabs.append-bottom-20 %li{class: ref_tab_class} diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml index 8a42f29b77..db2d7f2f4b 100644 --- a/app/views/ci/shared/_guide.html.haml +++ b/app/views/ci/shared/_guide.html.haml @@ -4,7 +4,7 @@ %ol %li Add at least one runner to the project. - Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions. + Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions. %li Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} diff --git a/spec/features/ci/runners_spec.rb b/spec/features/ci/runners_spec.rb deleted file mode 100644 index 15147f15eb..0000000000 --- a/spec/features/ci/runners_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'spec_helper' - -describe "Runners" do - let(:user) { create(:user) } - - before do - login_as(user) - end - - describe "specific runners" do - before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] - - @project2 = FactoryGirl.create :ci_project - @project2.gl_project.team << [user, :master] - - @shared_runner = FactoryGirl.create :ci_shared_runner - @specific_runner = FactoryGirl.create :ci_specific_runner - @specific_runner2 = FactoryGirl.create :ci_specific_runner - @project.runners << @specific_runner - @project2.runners << @specific_runner2 - end - - it "places runners in right places" do - visit ci_project_runners_path(@project) - expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name) - expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name) - expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name) - end - - it "enables specific runner for project" do - visit ci_project_runners_path(@project) - - within ".available-specific-runners" do - click_on "Enable for this project" - end - - expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name) - end - - it "disables specific runner for project" do - @project2.runners << @specific_runner - - visit ci_project_runners_path(@project) - - within ".activated-specific-runners" do - click_on "Disable for this project" - end - - expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name) - end - - it "removes specific runner for project if this is last project for that runners" do - visit ci_project_runners_path(@project) - - within ".activated-specific-runners" do - click_on "Remove runner" - end - - expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey - end - end - - describe "shared runners" do - before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] - end - - it "enables shared runners" do - visit ci_project_runners_path(@project) - - click_on "Enable shared runners" - - expect(@project.reload.shared_runners_enabled).to be_truthy - end - end - - describe "show page" do - before do - @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] - @specific_runner = FactoryGirl.create :ci_specific_runner - @project.runners << @specific_runner - end - - it "shows runner information" do - visit ci_project_runners_path(@project) - - click_on @specific_runner.short_sha - - expect(page).to have_content(@specific_runner.platform) - end - end -end From c876bfa0f28ddae7fbca314505f5b150b9cec194 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Sep 2015 11:01:45 +0200 Subject: [PATCH 053/543] Add missing spec file Signed-off-by: Dmitriy Zaporozhets --- spec/features/runners_spec.rb | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 spec/features/runners_spec.rb diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb new file mode 100644 index 0000000000..06adb7633b --- /dev/null +++ b/spec/features/runners_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe "Runners" do + include GitlabRoutingHelper + + let(:user) { create(:user) } + before { login_as(user) } + + describe "specific runners" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + + @project2 = FactoryGirl.create :ci_project + @project2.gl_project.team << [user, :master] + + @shared_runner = FactoryGirl.create :ci_shared_runner + @specific_runner = FactoryGirl.create :ci_specific_runner + @specific_runner2 = FactoryGirl.create :ci_specific_runner + @project.runners << @specific_runner + @project2.runners << @specific_runner2 + + visit runners_path(@project.gl_project) + end + + it "places runners in right places" do + expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name) + expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name) + expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name) + end + + it "enables specific runner for project" do + within ".available-specific-runners" do + click_on "Enable for this project" + end + + expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name) + end + + it "disables specific runner for project" do + @project2.runners << @specific_runner + visit runners_path(@project.gl_project) + + within ".activated-specific-runners" do + click_on "Disable for this project" + end + + expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name) + end + + it "removes specific runner for project if this is last project for that runners" do + within ".activated-specific-runners" do + click_on "Remove runner" + end + + expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey + end + end + + describe "shared runners" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + visit runners_path(@project.gl_project) + end + + it "enables shared runners" do + click_on "Enable shared runners" + expect(@project.reload.shared_runners_enabled).to be_truthy + end + end + + describe "show page" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + @specific_runner = FactoryGirl.create :ci_specific_runner + @project.runners << @specific_runner + visit runners_path(@project.gl_project) + end + + it "shows runner information" do + click_on @specific_runner.short_sha + expect(page).to have_content(@specific_runner.platform) + end + end +end From 74bdd67f2104069e68e0fb7938ae91667d04fc85 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Sep 2015 11:12:03 +0200 Subject: [PATCH 054/543] Update Source Sans Pro font: adds support for cyrillic characters Signed-off-by: Dmitriy Zaporozhets --- app/assets/fonts/SourceSansPro-Bold.ttf | Bin 148932 -> 291424 bytes app/assets/fonts/SourceSansPro-Light.ttf | Bin 150244 -> 293220 bytes app/assets/fonts/SourceSansPro-Regular.ttf | Bin 149972 -> 293956 bytes app/assets/fonts/SourceSansPro-Semibold.ttf | Bin 149636 -> 292404 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf index 50d81bdad58b06dbf4e3b6201cbdb31814aa5030..5d65c93242fc3776b379ab98bb5e6e33af4b25df 100755 GIT binary patch literal 291424 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
TV03jbdR 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 H{s8$K@VVub literal 148932 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

-PP`9ceiuw9(GT= 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

0T(9G^a8qt)`+5MkfPiowCC<|G& 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 HY0-ZHYYpa> diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf index 5020594826b603961399cec009188a8d11a437d5..86b00c067e0a15e5fdfe4f4d97e4665de8ad0410 100755 GIT binary patch literal 292404 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%0A4Z?eHaw!!7d?uQ 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 NZ!^EJg9yI>`4dh{B2EAR literal 149636 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

;uKu%NE38 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 Cmj74) From ce49e49bfb40872475b82b500f5e52f6ec36b81f Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 28 Sep 2015 11:26:42 +0200 Subject: [PATCH 055/543] edit action allign --- app/assets/stylesheets/base/mixins.scss | 10 +++++----- app/views/projects/_readme.html.haml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 5a7e79ddec..bba4728af3 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -243,11 +243,6 @@ } } -.pull-right .light .fa-pencil { - top: 20px; - position: relative; -} - @mixin input-big { height: 36px; padding: 5px 10px; @@ -300,3 +295,8 @@ } } } + +.fa-align { + top: 20px; + position: relative; +} \ No newline at end of file diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 5038edb95e..5bc1999ec9 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -5,7 +5,7 @@   - 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.fa-pencil + %i.fa-align.fa.fa-pencil .wiki = cache(readme_cache_key) do = render_readme(readme) From ad679127608ebb711bbd49268e3380a30aabb336 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 28 Sep 2015 12:03:41 +0200 Subject: [PATCH 056/543] !important removed from pre --- app/assets/stylesheets/base/mixins.scss | 6 ------ app/assets/stylesheets/generic/common.scss | 2 +- app/assets/stylesheets/highlight/white.scss | 7 ++++--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index bba4728af3..20c62250bc 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -157,12 +157,6 @@ pre { @include border-radius(2px); - - margin: 12px 0 12px 0 !important; - background-color: #f8fafc !important; - font-size: 13px !important; - color: #5b6169 !important; - line-height: 1.6em !important; } p > code { diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 48fad7701e..91dbe1ec24 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -313,7 +313,7 @@ table { } .wiki .highlight, .note-body .highlight { - margin-bottom: 9px; + margin: 12px 0 12px 0; } .wiki .code { diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 5de589109b..20a144ef95 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,9 +1,10 @@ /* https://github.com/aahan/pygments-github-style */ pre.code.highlight.white, .code.white { - - background-color: #fff; - color: #333; + background-color: #f8fafc; + font-size: 13px; + color: #5b6169; + line-height: 1.6em; .line-numbers, .line-numbers a { From 037defc7def3e2d0f2de4930516149d7567362fc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Sep 2015 17:19:20 +0200 Subject: [PATCH 057/543] Move CI variables page to project settings Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/variables_controller.rb | 33 ------------------- .../projects/application_controller.rb | 4 +++ .../projects/runners_controller.rb | 4 --- .../projects/variables_controller.rb | 25 ++++++++++++++ app/views/layouts/ci/_nav_project.html.haml | 5 --- .../layouts/nav/_project_settings.html.haml | 5 +++ .../{ci => projects}/variables/show.html.haml | 10 +++--- config/routes.rb | 2 +- spec/features/{ci => }/variables_spec.rb | 15 ++++----- 10 files changed, 47 insertions(+), 57 deletions(-) delete mode 100644 app/controllers/ci/variables_controller.rb create mode 100644 app/controllers/projects/variables_controller.rb rename app/views/{ci => projects}/variables/show.html.haml (77%) rename spec/features/{ci => }/variables_spec.rb (68%) diff --git a/CHANGELOG b/CHANGELOG index a0f1fa1208..7e31c2f57b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.1.0 (unreleased) - Add notes and SSL verification entries to hook APIs (Ben Boeckel) - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. - Move CI runners page to project settings area + - Move CI variables page to project settings area v 8.0.3 (unreleased) diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb deleted file mode 100644 index 9c6c775fde..0000000000 --- a/app/controllers/ci/variables_controller.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Ci - class VariablesController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_access_project! - before_action :authorize_manage_project! - - layout 'ci/project' - - def show - end - - def update - if project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, project) - - redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.' - else - render action: 'show' - end - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - - def project_params - params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) - end - end -end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 48c922f450..56a63ce975 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -31,4 +31,8 @@ class Projects::ApplicationController < ApplicationController def ci_enabled return render_404 unless @project.gitlab_ci? end + + def ci_project + @ci_project ||= @project.gitlab_ci_project + end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index d59884a1dd..6cb6e3ef6d 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -55,10 +55,6 @@ class Projects::RunnersController < Projects::ApplicationController protected - def ci_project - @ci_project = @project.gitlab_ci_project - end - def set_runner @runner ||= @ci_project.runners.find(params[:id]) end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb new file mode 100644 index 0000000000..d6561a45a7 --- /dev/null +++ b/app/controllers/projects/variables_controller.rb @@ -0,0 +1,25 @@ +class Projects::VariablesController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout 'project_settings' + + def show + end + + def update + if ci_project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, ci_project) + + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' + else + render action: 'show' + end + end + + private + + def project_params + params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + end +end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 9ebe7eabd8..4b0dc4fc2f 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -11,11 +11,6 @@ %span Commits %span.count= @project.commits.count - = nav_link path: 'variables#show' do - = link_to ci_project_variables_path(@project) do - = icon('code fw') - %span - Variables = nav_link path: 'web_hooks#index' do = link_to ci_project_web_hooks_path(@project) do = icon('link fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index a85dd71126..c8975fb849 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -40,3 +40,8 @@ = icon('cog fw') %span Runners + = nav_link(controller: :variables) do + = link_to namespace_project_variables_path(@project.namespace, @project) do + = icon('code fw') + %span + Variables diff --git a/app/views/ci/variables/show.html.haml b/app/views/projects/variables/show.html.haml similarity index 77% rename from app/views/ci/variables/show.html.haml rename to app/views/projects/variables/show.html.haml index ebf68341e0..29416a94ff 100644 --- a/app/views/ci/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,21 +1,21 @@ %h3.page-title Secret Variables -%p.light +%p.light These variables will be set to environment by the runner and will be hidden in the build log. %br - So you can use them for passwords, secret keys or whatever you want. + So you can use them for passwords, secret keys or whatever you want. %hr -= nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| += nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - if @project.errors.any? #error_explanation - %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" .alert.alert-error %ul - - @project.errors.full_messages.each do |msg| + - @ci_project.errors.full_messages.each do |msg| %li= msg = f.fields_for :variables do |variable_form| diff --git a/config/routes.rb b/config/routes.rb index 201add0233..776b606bf7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,7 +58,6 @@ Gitlab::Application.routes.draw do resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] - resource :variables, only: [:show, :update] end resource :user_sessions do @@ -591,6 +590,7 @@ Gitlab::Application.routes.draw do 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 :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resource :variables, only: [:show, :update] resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/spec/features/ci/variables_spec.rb b/spec/features/variables_spec.rb similarity index 68% rename from spec/features/ci/variables_spec.rb rename to spec/features/variables_spec.rb index e387b3be55..adb602f3ed 100644 --- a/spec/features/ci/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -1,28 +1,25 @@ require 'spec_helper' describe "Variables" do - let(:user) { create(:user) } - - before do - login_as(user) - end + let(:user) { create(:user) } + before { login_as(user) } describe "specific runners" do before do @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] + @gl_project = @project.gl_project + @gl_project.team << [user, :master] end it "creates variable", js: true do - visit ci_project_variables_path(@project) + visit namespace_project_variables_path(@gl_project.namespace, @gl_project) click_on "Add a variable" fill_in "Key", with: "SECRET_KEY" fill_in "Value", with: "SECRET_VALUE" click_on "Save changes" - + expect(page).to have_content("Variables were successfully updated.") expect(@project.variables.count).to eq(1) end - end end From 59582aae578453a7f9888e2380a7412c4bf14f4f Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 28 Sep 2015 17:58:30 +0200 Subject: [PATCH 058/543] page-title-fix --- app/assets/stylesheets/generic/typography.scss | 7 +++++++ app/views/projects/empty.html.haml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 0ccb21f3cd..9bd2ed0aef 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -12,6 +12,13 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: 600; +} + +.page-title-empty { + margin-top: 0px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; margin: 12px 7px 12px 7px; } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d8f9f692c0..e06454fd14 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -17,7 +17,7 @@ .prepend-top-20 .empty_wrapper - %h3.page-title + %h3.page-title-empty Command line instructions %div.git-empty %fieldset From 44b8844122029fceb45db89b3af554b77523f005 Mon Sep 17 00:00:00 2001 From: karen Carias Date: Mon, 28 Sep 2015 19:04:32 -0700 Subject: [PATCH 059/543] changed words --- doc/hooks/custom_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index dd3be70c90..548c484bc0 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore webhooks as an option if you do not have filesystem access. For a user-friendly Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. From 5b7e491c393f3ba436e45454a58709f6d70bbc5c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 28 Sep 2015 22:57:52 -0400 Subject: [PATCH 060/543] Update CHANGELOG for 8.0.3 [ci skip] --- CHANGELOG | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a0f1fa1208..fdb3063304 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.1.0 (unreleased) - Add user preference to view activities as default dashboard (Stan Hu) - - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) - Add option to admin area to sign in as a specific user (Pavel Forkert) - Show CI status on all pages where commits list is rendered - Automatically enable CI when push .gitlab-ci.yml file to repository @@ -17,7 +15,10 @@ v 8.1.0 (unreleased) - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. - Move CI runners page to project settings area -v 8.0.3 (unreleased) +v 8.0.3 + - Fix URL shown in Slack notifications + - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) + - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) v 8.0.2 - Fix default avatar not rendering in network graph (Stan Hu) From abae352e59b3fe8574249041508afd68bff8bab2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 28 Sep 2015 21:21:41 -0700 Subject: [PATCH 061/543] Link directly to the projects page instead of the root controller If "Your Projects' Activity" dashboard setting were selected, it would be impossible to navigate to the projects page since the redirection would always bounce back to the activity dashboard. Fixes bug introduced by !1446. --- app/views/layouts/nav/_dashboard.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 21d8655131..b1a1d53184 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do - = link_to root_path, title: 'Projects', data: {placement: 'right'} do + = link_to dashboard_projects_path, title: 'Projects', data: {placement: 'right'} do = icon('home fw') %span Projects From 2a0d4e7200d3e985552d887b9c9e14db073c70ab Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Sep 2015 10:37:31 +0200 Subject: [PATCH 062/543] Move CI triggers page to project settings area Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/triggers_controller.rb | 43 ------------------- .../projects/triggers_controller.rb | 35 +++++++++++++++ app/views/layouts/ci/_nav_project.html.haml | 5 --- .../layouts/nav/_project_settings.html.haml | 5 +++ .../triggers/_trigger.html.haml | 2 +- .../{ci => projects}/triggers/index.html.haml | 10 ++--- config/routes.rb | 3 +- spec/features/{ci => }/triggers_spec.rb | 9 ++-- 9 files changed, 53 insertions(+), 60 deletions(-) delete mode 100644 app/controllers/ci/triggers_controller.rb create mode 100644 app/controllers/projects/triggers_controller.rb rename app/views/{ci => projects}/triggers/_trigger.html.haml (53%) rename app/views/{ci => projects}/triggers/index.html.haml (77%) rename spec/features/{ci => }/triggers_spec.rb (68%) diff --git a/CHANGELOG b/CHANGELOG index f34f79d18d..181baf2488 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.1.0 (unreleased) - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. - Move CI runners page to project settings area - Move CI variables page to project settings area + - Move CI triggers page to project settings area v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb deleted file mode 100644 index a39cc5d3a5..0000000000 --- a/app/controllers/ci/triggers_controller.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Ci - class TriggersController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_access_project! - before_action :authorize_manage_project! - - layout 'ci/project' - - def index - @triggers = @project.triggers - @trigger = Ci::Trigger.new - end - - def create - @trigger = @project.triggers.new - @trigger.save - - if @trigger.valid? - redirect_to ci_project_triggers_path(@project) - else - @triggers = @project.triggers.select(&:persisted?) - render :index - end - end - - def destroy - trigger.destroy - - redirect_to ci_project_triggers_path(@project) - end - - private - - def trigger - @trigger ||= @project.triggers.find(params[:id]) - end - - def project - @project = Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb new file mode 100644 index 0000000000..782ebd01b0 --- /dev/null +++ b/app/controllers/projects/triggers_controller.rb @@ -0,0 +1,35 @@ +class Projects::TriggersController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout 'project_settings' + + def index + @triggers = @ci_project.triggers + @trigger = Ci::Trigger.new + end + + def create + @trigger = @ci_project.triggers.new + @trigger.save + + if @trigger.valid? + redirect_to namespace_project_triggers_path(@project.namespace, @project) + else + @triggers = @ci_project.triggers.select(&:persisted?) + render :index + end + end + + def destroy + trigger.destroy + + redirect_to namespace_project_triggers_path(@project.namespace, @project) + end + + private + + def trigger + @trigger ||= @ci_project.triggers.find(params[:id]) + end +end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 4b0dc4fc2f..2d3cc3cf98 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -16,11 +16,6 @@ = icon('link fw') %span Web Hooks - = nav_link path: 'triggers#index' do - = link_to ci_project_triggers_path(@project) do - = icon('retweet fw') - %span - Triggers = nav_link path: ['services#index', 'services#edit'] do = link_to ci_project_services_path(@project) do = icon('share fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index c8975fb849..28efb035d0 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -45,3 +45,8 @@ = icon('code fw') %span Variables + = nav_link path: 'triggers#index' do + = link_to namespace_project_triggers_path(@project.namespace, @project) do + = icon('retweet fw') + %span + Triggers diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml similarity index 53% rename from app/views/ci/triggers/_trigger.html.haml rename to app/views/projects/triggers/_trigger.html.haml index addfbfcb0d..48b3b5c992 100644 --- a/app/views/ci/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -11,4 +11,4 @@ %td .pull-right - = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped" + = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped" diff --git a/app/views/ci/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml similarity index 77% rename from app/views/ci/triggers/index.html.haml rename to app/views/projects/triggers/index.html.haml index 44374a1a4d..17dcb78e25 100644 --- a/app/views/ci/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -12,11 +12,11 @@ %th Token %th Last used %th - = render @triggers + = render partial: 'trigger', collection: @triggers, as: :trigger - else %h4 No triggers -= form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f| += form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create'), html: { class: 'form-horizontal' } do |f| .clearfix = f.submit "Add Trigger", class: 'btn btn-success pull-right' @@ -34,7 +34,7 @@ :plain curl -X POST \ -F token=TOKEN \ - #{ci_build_trigger_url(@project.id, 'REF_NAME')} + #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} %h3 Use .gitlab-ci.yml @@ -49,7 +49,7 @@ trigger: type: deploy script: - - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}" + - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}" %h3 Pass build variables @@ -64,4 +64,4 @@ curl -X POST \ -F token=TOKEN \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{ci_build_trigger_url(@project.id, 'REF_NAME')} + #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} diff --git a/config/routes.rb b/config/routes.rb index 776b606bf7..f7317fb5d9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,8 +53,6 @@ Gitlab::Application.routes.draw do end end - resources :triggers, only: [:index, :create, :destroy] - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -591,6 +589,7 @@ Gitlab::Application.routes.draw do resources :tags, only: [:index, :new, :create, :destroy], 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] resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/spec/features/ci/triggers_spec.rb b/spec/features/triggers_spec.rb similarity index 68% rename from spec/features/ci/triggers_spec.rb rename to spec/features/triggers_spec.rb index c6afeb7462..69492d5887 100644 --- a/spec/features/ci/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -1,13 +1,14 @@ require 'spec_helper' describe 'Triggers' do - let(:user) { create(:user) } + let(:user) { create(:user) } + before { login_as(user) } before do - login_as(user) @project = FactoryGirl.create :ci_project - @project.gl_project.team << [user, :master] - visit ci_project_triggers_path(@project) + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit namespace_project_triggers_path(@gl_project.namespace, @gl_project) end context 'create a trigger' do From 1530f68c9876bc8376bf4aa199c8abda570f5214 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 24 Sep 2015 17:09:33 +0200 Subject: [PATCH 063/543] WIP --- app/models/ci/build.rb | 3 +-- app/models/ci/commit.rb | 8 ++++++-- app/models/ci/project.rb | 4 ++-- app/models/project.rb | 2 ++ .../20150924125150_add_project_id_to_ci_tables.rb | 12 ++++++++++++ ...0924125436_migrate_project_id_for_ci_tables.rb | 15 +++++++++++++++ ...50924131004_add_ci_fields_to_projects_table.rb | 5 +++++ 7 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20150924125150_add_project_id_to_ci_tables.rb create mode 100644 db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb create mode 100644 db/migrate/20150924131004_add_ci_fields_to_projects_table.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8096d4fa5a..16ff6e3863 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -30,7 +30,6 @@ module Ci LAZY_ATTRIBUTES = ['trace'] belongs_to :commit, class_name: 'Ci::Commit' - belongs_to :project, class_name: 'Ci::Project' belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' @@ -137,7 +136,7 @@ module Ci state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :before_sha, :ref, + delegate :sha, :short_sha, :before_sha, :ref, :project, to: :commit, prefix: false def trace_html diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index f102d0a767..31638c7e1d 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -18,8 +18,8 @@ module Ci class Commit < ActiveRecord::Base extend Ci::Model - - belongs_to :project, class_name: 'Ci::Project' + + belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id has_many :builds, dependent: :destroy, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' @@ -32,6 +32,10 @@ module Ci sha[0...8] end + def project + @project ||= gl_project.gitlab_ci_project + end + def to_param sha end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 8fb54b90d6..ea7547f5d4 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -33,7 +33,7 @@ module Ci belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id - has_many :commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit' + has_many :commits, through: :gl_project, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' @@ -41,7 +41,7 @@ module Ci has_many :events, dependent: :destroy, class_name: 'Ci::Event' has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' - has_one :last_commit, -> { order 'ci_commits.created_at DESC' }, class_name: 'Ci::Commit' + has_one :last_commit, through: :gl_project, class_name: 'Ci::Commit' # Project services has_many :services, dependent: :destroy, class_name: 'Ci::Service' diff --git a/app/models/project.rb b/app/models/project.rb index e912c48467..efa573c82b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -118,6 +118,8 @@ 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 :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_one :last_commit, -> { order 'ci_commits.created_at DESC' }, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id diff --git a/db/migrate/20150924125150_add_project_id_to_ci_tables.rb b/db/migrate/20150924125150_add_project_id_to_ci_tables.rb new file mode 100644 index 0000000000..8b45bcf5fe --- /dev/null +++ b/db/migrate/20150924125150_add_project_id_to_ci_tables.rb @@ -0,0 +1,12 @@ +class AddProjectIdToCiTables < ActiveRecord::Migration + def up + add_column :ci_builds, :gl_project_id, :integer + add_column :ci_commits, :gl_project_id, :integer + add_column :ci_events, :gl_project_id, :integer + add_column :ci_runner_projects, :gl_project_id, :integer + add_column :ci_services, :gl_project_id, :integer + add_column :ci_triggers, :gl_project_id, :integer + add_column :ci_variables, :gl_project_id, :integer + add_column :ci_web_hooks, :gl_project_id, :integer + end +end diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb new file mode 100644 index 0000000000..28d63c8c84 --- /dev/null +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb @@ -0,0 +1,15 @@ +class MigrateProjectIdForCiTables < ActiveRecord::Migration + TABLES = %w(ci_builds ci_commits ci_events ci_runner_projects + ci_services ci_triggers ci_variables ci_web_hooks) + + def up + TABLES.each do |table| + execute( + "UPDATE #{table} " + + "JOIN ci_projects ON ci_projects.id = #{table}.project_id " + + "SET gl_project_id=ci_projects.gitlab_id " + + "WHERE gl_project_id IS NULL" + ) + end + end +end diff --git a/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb b/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb new file mode 100644 index 0000000000..bb97ec3005 --- /dev/null +++ b/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb @@ -0,0 +1,5 @@ +class AddCiFieldsToProjectsTable < ActiveRecord::Migration + def up + add_column :projects, :shared_runners_enabled, :boolean, default: false + end +end From 30c78e70cba395c1336611c58891a75473f8a037 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 13:14:34 +0200 Subject: [PATCH 064/543] WIP --- app/controllers/ci/application_controller.rb | 8 +- app/models/ci/build.rb | 3 +- app/models/ci/commit.rb | 10 +-- app/models/ci/project.rb | 29 ++++--- app/models/project.rb | 1 + db/schema.rb | 80 ++++++++++--------- .../controllers/ci/commits_controller_spec.rb | 12 +-- spec/factories/ci/commits.rb | 2 + spec/factories/ci/projects.rb | 2 +- spec/features/ci/admin/builds_spec.rb | 3 +- spec/features/ci/builds_spec.rb | 19 ++--- spec/features/ci/commits_spec.rb | 11 +-- spec/mailers/ci/notify_spec.rb | 3 +- spec/models/ci/build_spec.rb | 3 +- spec/models/ci/commit_spec.rb | 23 +++--- spec/models/ci/mail_service_spec.rb | 18 +++-- .../project_services/hip_chat_service_spec.rb | 3 +- .../ci/project_services/slack_service_spec.rb | 3 +- spec/models/ci/project_spec.rb | 3 +- spec/models/ci/service_spec.rb | 3 +- spec/requests/ci/api/builds_spec.rb | 16 ++-- spec/requests/ci/api/commits_spec.rb | 3 +- spec/requests/ci/api/triggers_spec.rb | 3 +- spec/requests/ci/builds_spec.rb | 3 +- spec/requests/ci/commits_spec.rb | 3 +- .../ci/create_trigger_request_service_spec.rb | 11 +-- .../ci/image_for_build_service_spec.rb | 3 +- .../ci/register_build_service_spec.rb | 5 +- spec/services/ci/web_hook_service_spec.rb | 3 +- 29 files changed, 154 insertions(+), 135 deletions(-) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index d8227e632e..da77e2b94e 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -11,10 +11,10 @@ module Ci private def check_enable_flag! - unless current_application_settings.ci_enabled - redirect_to(disabled_ci_projects_path) - return - end + # unless current_application_settings.ci_enabled + # redirect_to(disabled_ci_projects_path) + # return + # end end def authenticate_public_page! diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 16ff6e3863..9ac47ccfe4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -79,7 +79,6 @@ module Ci new_build.commands = build.commands new_build.tag_list = build.tag_list new_build.commit_id = build.commit_id - new_build.project_id = build.project_id new_build.name = build.name new_build.allow_failure = build.allow_failure new_build.stage = build.stage @@ -187,7 +186,7 @@ module Ci end def project_id - commit.project_id + commit.gl_project.gitlab_id end def project_name diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 31638c7e1d..9a71978764 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -32,14 +32,15 @@ module Ci sha[0...8] end - def project - @project ||= gl_project.gitlab_ci_project - end - def to_param sha end + def project + @project ||= gl_project.gitlab_ci_project + @project ||= gl_project.create_gitlab_ci_project + end + def last_build builds.order(:id).last end @@ -115,7 +116,6 @@ module Ci builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) builds_attrs.map do |build_attrs| builds.create!({ - project: project, name: build_attrs[:name], commands: build_attrs[:script], tag_list: build_attrs[:tags], diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index ea7547f5d4..89bbbea5c5 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -33,15 +33,12 @@ module Ci belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id - has_many :commits, through: :gl_project, class_name: 'Ci::Commit', foreign_key: :gl_project_id - has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' has_many :events, dependent: :destroy, class_name: 'Ci::Event' has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' - has_one :last_commit, through: :gl_project, class_name: 'Ci::Commit' # Project services has_many :services, dependent: :destroy, class_name: 'Ci::Service' @@ -51,17 +48,19 @@ module Ci accepts_nested_attributes_for :variables, allow_destroy: true + delegate :commits, :builds, :last_commit, to: :gl_project + # # Validations # validates_presence_of :name, :timeout, :token, :default_ref, - :path, :ssh_url_to_repo, :gitlab_id + :path, :ssh_url_to_repo, :gitlab_id validates_uniqueness_of :gitlab_id validates :polling_interval, - presence: true, - if: ->(project) { project.always_build.present? } + presence: true, + if: ->(project) { project.always_build.present? } scope :public_only, ->() { where(public: true) } @@ -79,12 +78,12 @@ module Ci def parse(project) params = { - name: project.name_with_namespace, - gitlab_id: project.id, - path: project.path_with_namespace, - default_ref: project.default_branch || 'master', - ssh_url_to_repo: project.ssh_url_to_repo, - email_add_pusher: current_application_settings.add_pusher, + name: project.name_with_namespace, + gitlab_id: project.id, + path: project.path_with_namespace, + default_ref: project.default_branch || 'master', + ssh_url_to_repo: project.ssh_url_to_repo, + email_add_pusher: current_application_settings.add_pusher, email_only_broken_builds: current_application_settings.all_broken_builds, } @@ -125,10 +124,14 @@ module Ci def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? + self.default_ref ||= 'master' + self.name ||= gl_project.name_with_namespace + self.path ||= gl_project.path_with_namespace + self.ssh_url_to_repo ||= gl_project.ssh_url_to_repo end def tracked_refs - @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip} + @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip } end def valid_token? token diff --git a/app/models/project.rb b/app/models/project.rb index efa573c82b..9ecf16d981 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -119,6 +119,7 @@ class Project < ActiveRecord::Base has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user has_many :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 :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' has_one :last_commit, -> { order 'ci_commits.created_at DESC' }, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/db/schema.rb b/db/schema.rb index 01ccda7a75..89aafba511 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,10 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150920161119) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" +ActiveRecord::Schema.define(version: 20150924131004) do create_table "abuse_reports", force: true do |t| t.integer "reporter_id" @@ -45,8 +42,8 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" - t.text "help_page_text" t.boolean "ci_enabled", default: true, null: false + t.text "help_page_text" end create_table "audit_events", force: true do |t| @@ -85,21 +82,22 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.integer "project_id" t.string "status" t.datetime "finished_at" - t.text "trace" + t.text "trace", limit: 2147483647 t.datetime "created_at" t.datetime "updated_at" t.datetime "started_at" t.integer "runner_id" - t.float "coverage" + t.float "coverage", limit: 24 t.integer "commit_id" t.text "commands" t.integer "job_id" t.string "name" - t.boolean "deploy", default: false + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false + t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" + t.integer "gl_project_id" end add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree @@ -112,12 +110,13 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.string "ref" t.string "sha" t.string "before_sha" - t.text "push_data" + t.text "push_data", limit: 16777215 t.datetime "created_at" t.datetime "updated_at" - t.boolean "tag", default: false + t.boolean "tag", default: false t.text "yaml_errors" t.datetime "committed_at" + t.integer "gl_project_id" end add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree @@ -133,6 +132,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.text "description" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree @@ -180,10 +180,11 @@ ActiveRecord::Schema.define(version: 20150920161119) do end create_table "ci_runner_projects", force: true do |t| - t.integer "runner_id", null: false - t.integer "project_id", null: false + t.integer "runner_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree @@ -207,11 +208,12 @@ ActiveRecord::Schema.define(version: 20150920161119) do create_table "ci_services", force: true do |t| t.string "type" t.string "title" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" + t.integer "gl_project_id" end add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree @@ -256,10 +258,11 @@ ActiveRecord::Schema.define(version: 20150920161119) do create_table "ci_triggers", force: true do |t| t.string "token" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree @@ -271,15 +274,17 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.text "encrypted_value" t.string "encrypted_value_salt" t.string "encrypted_value_iv" + t.integer "gl_project_id" end add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree create_table "ci_web_hooks", force: true do |t| - t.string "url", null: false - t.integer "project_id", null: false + t.string "url", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end create_table "deploy_keys_projects", force: true do |t| @@ -425,9 +430,9 @@ ActiveRecord::Schema.define(version: 20150920161119) do create_table "merge_request_diffs", force: true do |t| t.string "state" - t.text "st_commits" - t.text "st_diffs" - t.integer "merge_request_id", null: false + t.text "st_commits", limit: 2147483647 + t.text "st_diffs", limit: 2147483647 + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -509,8 +514,8 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false - t.text "st_diff" + t.boolean "system", default: false, null: false + t.text "st_diff", limit: 2147483647 t.integer "updated_by_id" end @@ -579,25 +584,26 @@ ActiveRecord::Schema.define(version: 20150920161119) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", limit: 24, default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 + t.boolean "shared_runners_enabled", default: false end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -649,15 +655,15 @@ ActiveRecord::Schema.define(version: 20150920161119) do create_table "snippets", force: true do |t| t.string "title" - t.text "content" - t.integer "author_id", null: false + t.text "content", limit: 2147483647 + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" t.string "type" - t.integer "visibility_level", default: 0, null: false + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb index b71e750573..cc39ce7687 100644 --- a/spec/controllers/ci/commits_controller_spec.rb +++ b/spec/controllers/ci/commits_controller_spec.rb @@ -1,14 +1,10 @@ require "spec_helper" describe Ci::CommitsController do - before do - @project = FactoryGirl.create :ci_project - end - describe "GET /status" do it "returns status of commit" do - commit = FactoryGirl.create :ci_commit, project: @project - get :status, id: commit.sha, ref_id: commit.ref, project_id: @project.id + commit = FactoryGirl.create :ci_commit + get :status, id: commit.sha, ref_id: commit.ref, project_id: commit.project.id expect(response).to be_success expect(response.code).to eq('200') @@ -16,8 +12,8 @@ describe Ci::CommitsController do end it "returns not_found status" do - commit = FactoryGirl.create :ci_commit, project: @project - get :status, id: commit.sha, ref_id: "deploy", project_id: @project.id + commit = FactoryGirl.create :ci_commit + get :status, id: commit.sha, ref_id: "deploy", project_id: commit.project.id expect(response).to be_success expect(response.code).to eq('200') diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 70930c789c..9c7a0e9cbe 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -51,6 +51,8 @@ FactoryGirl.define do } end + gl_project factory: :empty_project + factory :ci_commit_without_jobs do after(:create) do |commit, evaluator| commit.push_data[:ci_yaml_file] = YAML.dump({}) diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb index e6bd0685f8..d492fe8209 100644 --- a/spec/factories/ci/projects.rb +++ b/spec/factories/ci/projects.rb @@ -43,7 +43,7 @@ FactoryGirl.define do "git@demo.gitlab.com:gitlab/gitlab-shell#{n}.git" end - gl_project factory: :project + gl_project factory: :empty_project factory :ci_project do token 'iPWx6WM4lhHNedGfBpPJNP' diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb index 88ef9c144a..ee757206a0 100644 --- a/spec/features/ci/admin/builds_spec.rb +++ b/spec/features/ci/admin/builds_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' describe "Admin Builds" do - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } let(:build) { FactoryGirl.create :ci_build, commit: commit } before do diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb index 2f020e524e..d65699dbef 100644 --- a/spec/features/ci/builds_spec.rb +++ b/spec/features/ci/builds_spec.rb @@ -3,16 +3,15 @@ require 'spec_helper' describe "Builds" do context :private_project do before do - @project = FactoryGirl.create :ci_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit login_as :user - @project.gl_project.team << [@user, :master] + @commit.project.gl_project.team << [@user, :master] end describe "GET /:project/builds/:id" do before do - visit ci_project_build_path(@project, @build) + visit ci_project_build_path(@commit.project, @build) end it { expect(page).to have_content @commit.sha[0..7] } @@ -23,7 +22,7 @@ describe "Builds" do describe "GET /:project/builds/:id/cancel" do before do @build.run! - visit cancel_ci_project_build_path(@project, @build) + visit cancel_ci_project_build_path(@commit.project, @build) end it { expect(page).to have_content 'canceled' } @@ -33,7 +32,7 @@ describe "Builds" do describe "POST /:project/builds/:id/retry" do before do @build.cancel! - visit ci_project_build_path(@project, @build) + visit ci_project_build_path(@commit.project, @build) click_link 'Retry' end @@ -45,13 +44,15 @@ describe "Builds" do context :public_project do describe "Show page public accessible" do before do - @project = FactoryGirl.create :ci_public_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit + @commit.project.public = true + @commit.project.save + @runner = FactoryGirl.create :ci_specific_runner @build = FactoryGirl.create :ci_build, commit: @commit, runner: @runner stub_gitlab_calls - visit ci_project_build_path(@project, @build) + visit ci_project_build_path(@commit.project, @build) end it { expect(page).to have_content @commit.sha[0..7] } diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb index 40a62ca457..657a9dabe9 100644 --- a/spec/features/ci/commits_spec.rb +++ b/spec/features/ci/commits_spec.rb @@ -5,11 +5,10 @@ describe "Commits" do context "Authenticated user" do before do - @project = FactoryGirl.create :ci_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit login_as :user - @project.gl_project.team << [@user, :master] + @commit.project.gl_project.team << [@user, :master] end describe "GET /:project/commits/:sha" do @@ -51,8 +50,10 @@ describe "Commits" do context "Public pages" do before do - @project = FactoryGirl.create :ci_public_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit + @commit.project.public = true + @commit.project.save + @build = FactoryGirl.create :ci_build, commit: @commit end diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb index 20d8ddcd13..b83fb41603 100644 --- a/spec/mailers/ci/notify_spec.rb +++ b/spec/mailers/ci/notify_spec.rb @@ -5,8 +5,7 @@ describe Ci::Notify do include EmailSpec::Matchers before do - @project = FactoryGirl.create :ci_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index ce80115204..82623bd819 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -27,7 +27,8 @@ require 'spec_helper' describe Ci::Build do let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } it { is_expected.to belong_to(:commit) } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 586c9dc23a..c277cbd8bc 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -18,9 +18,8 @@ require 'spec_helper' describe Ci::Commit do - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } - let(:commit_with_project) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } + let(:commit_with_project) { FactoryGirl.create :ci_commit } let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) } it { is_expected.to belong_to(:project) } @@ -65,7 +64,8 @@ describe Ci::Commit do project = FactoryGirl.create :ci_project, email_add_pusher: true, email_recipients: '' - commit = FactoryGirl.create :ci_commit, project: project + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project expected = 'commit_pusher_email' allow(commit).to receive(:push_data) { { user_email: expected } } expect(commit.project_recipients).to eq([expected]) @@ -75,7 +75,8 @@ describe Ci::Commit do project = FactoryGirl.create :ci_project, email_add_pusher: true, email_recipients: 'rec1 rec2' - commit = FactoryGirl.create :ci_commit, project: project + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project expected = 'commit_pusher_email' allow(commit).to receive(:push_data) { { user_email: expected } } expect(commit.project_recipients).to eq(['rec1', 'rec2', expected]) @@ -85,7 +86,8 @@ describe Ci::Commit do project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'rec1 rec2' - commit = FactoryGirl.create :ci_commit, project: project + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, project: gl_project expect(commit.project_recipients).to eq(['rec1', 'rec2']) end @@ -93,7 +95,8 @@ describe Ci::Commit do project = FactoryGirl.create :ci_project, email_add_pusher: true, email_recipients: 'rec1 rec1 rec2' - commit = FactoryGirl.create :ci_commit, project: project + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, project: gl_project expected = 'rec2' allow(commit).to receive(:push_data) { { user_email: expected } } expect(commit.project_recipients).to eq(['rec1', 'rec2']) @@ -219,8 +222,7 @@ describe Ci::Commit do end describe "#finished_at" do - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } it "returns finished_at of latest build" do build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60 @@ -238,7 +240,8 @@ describe Ci::Commit do describe "coverage" do let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } it "calculates average when there are two builds with coverage" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit diff --git a/spec/models/ci/mail_service_spec.rb b/spec/models/ci/mail_service_spec.rb index b5f37b349d..0d9f85959b 100644 --- a/spec/models/ci/mail_service_spec.rb +++ b/spec/models/ci/mail_service_spec.rb @@ -32,7 +32,8 @@ describe Ci::MailService do describe 'failed build' do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) } before do @@ -54,7 +55,8 @@ describe Ci::MailService do describe 'successfull build' do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } before do @@ -81,7 +83,8 @@ describe Ci::MailService do email_only_broken_builds: false, email_recipients: "jeroen@example.com") end - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } before do @@ -109,7 +112,8 @@ describe Ci::MailService do email_only_broken_builds: true, email_recipients: "jeroen@example.com") end - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } before do @@ -137,7 +141,8 @@ describe Ci::MailService do email_only_broken_builds: false, email_recipients: "jeroen@example.com") end - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } before do @@ -159,7 +164,8 @@ describe Ci::MailService do email_only_broken_builds: true, email_recipients: "jeroen@example.com") end - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) } before do diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb index 063d46b84d..8f91a986b3 100644 --- a/spec/models/ci/project_services/hip_chat_service_spec.rb +++ b/spec/models/ci/project_services/hip_chat_service_spec.rb @@ -33,8 +33,7 @@ describe Ci::HipChatService do describe "Execute" do let(:service) { Ci::HipChatService.new } - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' } diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb index 0524f47243..80adadc591 100644 --- a/spec/models/ci/project_services/slack_service_spec.rb +++ b/spec/models/ci/project_services/slack_service_spec.rb @@ -31,8 +31,7 @@ describe Ci::SlackService do describe "Execute" do let(:slack) { Ci::SlackService.new } - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } let(:notify_only_broken_builds) { false } diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index 261ea69f5b..a45f1094fd 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -81,10 +81,11 @@ describe Ci::Project do context :valid_project do 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) } context :project_with_commit_and_builds do before do - commit = FactoryGirl.create(:ci_commit, project: project) FactoryGirl.create(:ci_build, commit: commit) end diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb index 2c575056b0..f6354f3cbc 100644 --- a/spec/models/ci/service_spec.rb +++ b/spec/models/ci/service_spec.rb @@ -29,8 +29,7 @@ describe Ci::Service do end describe "Testable" do - let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:commit) { FactoryGirl.create :ci_commit } let(:build) { FactoryGirl.create :ci_build, commit: commit } before do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c25d182330..bad250fbf4 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -5,10 +5,12 @@ describe Ci::API::API do let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } let(:project) { FactoryGirl.create(:ci_project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } describe "Builds API for runners" do let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") } + let(:shared_gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: shared_project) } before do FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id @@ -16,7 +18,7 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) commit.create_builds build = commit.builds.first @@ -34,7 +36,7 @@ describe Ci::API::API do end it "should return 404 error if no builds for specific runner" do - commit = FactoryGirl.create(:ci_commit, project: shared_project) + commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project) FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) post ci_api("/builds/register"), token: runner.token @@ -43,7 +45,7 @@ describe Ci::API::API do end it "should return 404 error if no builds for shared runner" do - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) post ci_api("/builds/register"), token: shared_runner.token @@ -52,7 +54,7 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) commit.create_builds post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -62,7 +64,7 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) commit.create_builds project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") @@ -77,7 +79,7 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) commit.create_builds(trigger_request) @@ -95,7 +97,7 @@ describe Ci::API::API do end describe "PUT /builds/:id" do - let(:commit) { FactoryGirl.create(:ci_commit, project: project)} + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)} let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } it "should update a running build" do diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb index e89b665149..a41c321a30 100644 --- a/spec/requests/ci/api/commits_spec.rb +++ b/spec/requests/ci/api/commits_spec.rb @@ -4,7 +4,8 @@ describe Ci::API::API, 'Commits' do include ApiHelpers let(:project) { FactoryGirl.create(:ci_project) } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } let(:options) do { diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index ff6fdbdd6f..bbe98e7dac 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -6,6 +6,7 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } let!(:project) { FactoryGirl.create(:ci_project) } + let!(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let!(:project2) { FactoryGirl.create(:ci_project) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do @@ -33,7 +34,7 @@ describe Ci::API::API do context 'Have a commit' do before do - @commit = FactoryGirl.create(:ci_commit, project: project) + @commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) end it 'should create builds' do diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb index 998c386ead..32ca52686a 100644 --- a/spec/requests/ci/builds_spec.rb +++ b/spec/requests/ci/builds_spec.rb @@ -2,8 +2,7 @@ require 'spec_helper' describe "Builds" do before do - @project = FactoryGirl.create :ci_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit end diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb index fb31767033..4d7f132023 100644 --- a/spec/requests/ci/commits_spec.rb +++ b/spec/requests/ci/commits_spec.rb @@ -2,8 +2,7 @@ require 'spec_helper' describe "Commits" do before do - @project = FactoryGirl.create :ci_project - @commit = FactoryGirl.create :ci_commit, project: @project + @commit = FactoryGirl.create :ci_commit end describe "GET /:project/refs/:ref_name/commits/:id/status.json" do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index d12cd9773d..525a24cc20 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService do let(:service) { Ci::CreateTriggerRequestService.new } let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:trigger) { FactoryGirl.create :ci_trigger, project: project } describe :execute do @@ -10,7 +11,7 @@ describe Ci::CreateTriggerRequestService do subject { service.execute(project, trigger, 'master') } before do - @commit = FactoryGirl.create :ci_commit, project: project + @commit = FactoryGirl.create :ci_commit, gl_project: gl_project end it { expect(subject).to be_kind_of(Ci::TriggerRequest) } @@ -27,7 +28,7 @@ describe Ci::CreateTriggerRequestService do subject { service.execute(project, trigger, 'master') } before do - FactoryGirl.create :ci_commit_without_jobs, project: project + FactoryGirl.create :ci_commit_without_jobs, gl_project: gl_project end it { expect(subject).to be_nil } @@ -37,9 +38,9 @@ describe Ci::CreateTriggerRequestService do subject { service.execute(project, trigger, 'master') } before do - @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project - @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, project: project + @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: gl_project + @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, gl_project: gl_project end context 'retries latest one' do diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 7565eb8f03..d7242d684c 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -4,7 +4,8 @@ module Ci describe ImageForBuildService do let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:ci_project) } - let(:commit) { FactoryGirl.create(:ci_commit, project: project, ref: 'master') } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') } let(:build) { FactoryGirl.create(:ci_build, commit: commit) } describe :execute do diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 7b5af6c3dd..96b7e2db3e 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' module Ci describe RegisterBuildService do let!(:service) { RegisterBuildService.new } - let!(:project) { FactoryGirl.create :ci_project } - let!(:commit) { FactoryGirl.create :ci_commit, project: project } - let!(:pending_build) { FactoryGirl.create :ci_build, project: project, commit: commit } + let!(:commit) { FactoryGirl.create :ci_commit } + let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb index cebdd145e4..aa48fcbcbf 100644 --- a/spec/services/ci/web_hook_service_spec.rb +++ b/spec/services/ci/web_hook_service_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Ci::WebHookService do let(:project) { FactoryGirl.create :ci_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } let(:hook) { FactoryGirl.create :ci_web_hook, project: project } From 0e3381470870732dff69c9298131062f786d55e7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 13:35:26 +0200 Subject: [PATCH 065/543] Fix tests --- app/models/ci/build.rb | 2 +- app/models/ci/commit.rb | 27 ++++++---- app/models/project.rb | 6 +++ spec/models/ci/commit_spec.rb | 12 +++-- .../project_services/hip_chat_message_spec.rb | 6 +-- .../project_services/hip_chat_service_spec.rb | 4 +- .../ci/project_services/slack_message_spec.rb | 6 +-- .../ci/project_services/slack_service_spec.rb | 4 +- spec/models/ci/project_spec.rb | 49 ++++++++++++------- spec/models/ci/service_spec.rb | 2 +- .../ci/register_build_service_spec.rb | 4 +- 11 files changed, 73 insertions(+), 49 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9ac47ccfe4..cda4fdd498 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -186,7 +186,7 @@ module Ci end def project_id - commit.gl_project.gitlab_id + commit.project.id end def project_name diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 9a71978764..a6556690b9 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -37,8 +37,15 @@ module Ci end def project - @project ||= gl_project.gitlab_ci_project - @project ||= gl_project.create_gitlab_ci_project + unless @project + gl_project.ensure_ci_project + @project = gl_project.gitlab_ci_project + end + @project + end + + def project_id + project.id end def last_build @@ -116,14 +123,14 @@ module Ci builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) builds_attrs.map do |build_attrs| builds.create!({ - name: build_attrs[:name], - commands: build_attrs[:script], - tag_list: build_attrs[:tags], - options: build_attrs[:options], - allow_failure: build_attrs[:allow_failure], - stage: build_attrs[:stage], - trigger_request: trigger_request, - }) + name: build_attrs[:name], + commands: build_attrs[:script], + tag_list: build_attrs[:tags], + options: build_attrs[:options], + allow_failure: build_attrs[:allow_failure], + stage: build_attrs[:stage], + trigger_request: trigger_request, + }) end end diff --git a/app/models/project.rb b/app/models/project.rb index 9ecf16d981..ddf8526d6c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -748,6 +748,12 @@ class Project < ActiveRecord::Base gitlab_ci_project.commits.find_by(sha: sha) if gitlab_ci? end + def ensure_ci_project + unless gitlab_ci_project + create_gitlab_ci_project + end + end + def enable_ci(user) # Enable service service = gitlab_ci_service || create_gitlab_ci_service diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index c277cbd8bc..5429151c8d 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -18,11 +18,13 @@ require 'spec_helper' describe Ci::Commit do - let(:commit) { FactoryGirl.create :ci_commit } - let(:commit_with_project) { FactoryGirl.create :ci_commit } + 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 } + let(:commit_with_project) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) } - it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:gl_project) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :before_sha } it { is_expected.to validate_presence_of :sha } @@ -87,7 +89,7 @@ describe Ci::Commit do email_add_pusher: false, email_recipients: 'rec1 rec2' gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, project: gl_project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project expect(commit.project_recipients).to eq(['rec1', 'rec2']) end @@ -96,7 +98,7 @@ describe Ci::Commit do email_add_pusher: true, email_recipients: 'rec1 rec1 rec2' gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, project: gl_project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project expected = 'rec2' allow(commit).to receive(:push_data) { { user_email: expected } } expect(commit.project_recipients).to eq(['rec1', 'rec2']) diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb index 49ac086025..1903c03692 100644 --- a/spec/models/ci/project_services/hip_chat_message_spec.rb +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -3,10 +3,8 @@ require 'spec_helper' describe Ci::HipChatMessage do subject { Ci::HipChatMessage.new(build) } - let(:project) { FactoryGirl.create(:ci_project) } - context "One build" do - let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) } + let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } let(:build) do commit.create_builds @@ -37,7 +35,7 @@ describe Ci::HipChatMessage do end context "Several builds" do - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) } + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } let(:build) do commit.builds.first diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb index 8f91a986b3..d9ccc855ed 100644 --- a/spec/models/ci/project_services/hip_chat_service_spec.rb +++ b/spec/models/ci/project_services/hip_chat_service_spec.rb @@ -39,8 +39,8 @@ describe Ci::HipChatService do before do allow(service).to receive_messages( - project: project, - project_id: project.id, + project: commit.project, + project_id: commit.project_id, notify_only_broken_builds: false, hipchat_room: 123, hipchat_token: 'a1b2c3d4e5f6' diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb index f533590372..7b541802d7 100644 --- a/spec/models/ci/project_services/slack_message_spec.rb +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -3,10 +3,8 @@ require 'spec_helper' describe Ci::SlackMessage do subject { Ci::SlackMessage.new(commit) } - let(:project) { FactoryGirl.create :ci_project } - context "One build" do - let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) } + let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } let(:build) do commit.create_builds @@ -43,7 +41,7 @@ describe Ci::SlackMessage do end context "Several builds" do - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) } + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } context 'when all matrix builds succeeded' do let(:color) { 'good' } diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb index 80adadc591..1ac7dfe568 100644 --- a/spec/models/ci/project_services/slack_service_spec.rb +++ b/spec/models/ci/project_services/slack_service_spec.rb @@ -38,8 +38,8 @@ describe Ci::SlackService do before do allow(slack).to receive_messages( - project: project, - project_id: project.id, + project: commit.project, + project_id: commit.project_id, webhook: webhook_url, notify_only_broken_builds: notify_only_broken_builds ) diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index a45f1094fd..b83eccb94e 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -28,9 +28,17 @@ require 'spec_helper' describe Ci::Project do + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let (:gl_project) { } subject { FactoryGirl.build :ci_project } - it { is_expected.to have_many(:commits) } + it { is_expected.to have_many(:runner_projects) } + it { is_expected.to have_many(:runners) } + it { is_expected.to have_many(:web_hooks) } + it { is_expected.to have_many(:events) } + it { is_expected.to have_many(:variables) } + it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:services) } it { is_expected.to validate_presence_of :name } it { is_expected.to validate_presence_of :timeout } @@ -50,41 +58,44 @@ describe Ci::Project do describe "ordered_by_last_commit_date" do it "returns ordered projects" do - newest_project = FactoryGirl.create :ci_project - oldest_project = FactoryGirl.create :ci_project - project_without_commits = FactoryGirl.create :ci_project + newest_project = FactoryGirl.create :empty_project + newest_project.ensure_ci_project + oldest_project = FactoryGirl.create :empty_project + oldest_project.ensure_ci_project + project_without_commits = FactoryGirl.create :empty_project + project_without_commits.ensure_ci_project - FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: newest_project - FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: oldest_project + FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project + FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project - expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_project, oldest_project, project_without_commits]) + expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_project.gitlab_ci_project, oldest_project.gitlab_ci_project, project_without_commits.gitlab_ci_project]) end end describe 'ordered commits' do - let(:project) { FactoryGirl.create :ci_project } + let(:project) { FactoryGirl.create :empty_project } it 'returns ordered list of commits' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project - expect(project.commits).to eq([commit2, commit1]) + 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.commits).to eq([commit2.project, commit1.project]) end it 'returns commits ordered by committed_at and id, with nulls last' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project - commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - expect(project.commits).to eq([commit2, commit4, commit3, commit1]) + 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.commits).to eq([commit2.project, commit4.project, commit3.project, commit1.project]) end end context :valid_project do - 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) } + let(:commit) { FactoryGirl.create(:ci_commit) } context :project_with_commit_and_builds do + let(:project) { commit.project } + before do FactoryGirl.create(:ci_build, commit: commit) end diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb index f6354f3cbc..2df70e8888 100644 --- a/spec/models/ci/service_spec.rb +++ b/spec/models/ci/service_spec.rb @@ -34,7 +34,7 @@ describe Ci::Service do before do allow(@service).to receive_messages( - project: project + project: commit.project ) build @testable = @service.can_test? diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 96b7e2db3e..9057791ca4 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' module Ci describe RegisterBuildService do let!(:service) { RegisterBuildService.new } - let!(:commit) { FactoryGirl.create :ci_commit } + 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 } let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } From f88d9cee0fe7fc6436a14051eb20562b6b4d7d86 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 13:39:13 +0200 Subject: [PATCH 066/543] Fix migrations --- .../20150924125150_add_project_id_to_ci_commit.rb | 5 +++++ .../20150924125150_add_project_id_to_ci_tables.rb | 12 ------------ ...924125436_migrate_project_id_for_ci_commits.rb | 10 ++++++++++ ...0924125436_migrate_project_id_for_ci_tables.rb | 15 --------------- ...50924131004_add_ci_fields_to_projects_table.rb | 5 ----- 5 files changed, 15 insertions(+), 32 deletions(-) create mode 100644 db/migrate/20150924125150_add_project_id_to_ci_commit.rb delete mode 100644 db/migrate/20150924125150_add_project_id_to_ci_tables.rb create mode 100644 db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb delete mode 100644 db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb delete mode 100644 db/migrate/20150924131004_add_ci_fields_to_projects_table.rb diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb new file mode 100644 index 0000000000..1a761fe0f8 --- /dev/null +++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb @@ -0,0 +1,5 @@ +class AddProjectIdToCiCommit < ActiveRecord::Migration + def up + add_column :ci_commits, :gl_project_id, :integer + end +end diff --git a/db/migrate/20150924125150_add_project_id_to_ci_tables.rb b/db/migrate/20150924125150_add_project_id_to_ci_tables.rb deleted file mode 100644 index 8b45bcf5fe..0000000000 --- a/db/migrate/20150924125150_add_project_id_to_ci_tables.rb +++ /dev/null @@ -1,12 +0,0 @@ -class AddProjectIdToCiTables < ActiveRecord::Migration - def up - add_column :ci_builds, :gl_project_id, :integer - add_column :ci_commits, :gl_project_id, :integer - add_column :ci_events, :gl_project_id, :integer - add_column :ci_runner_projects, :gl_project_id, :integer - add_column :ci_services, :gl_project_id, :integer - add_column :ci_triggers, :gl_project_id, :integer - add_column :ci_variables, :gl_project_id, :integer - add_column :ci_web_hooks, :gl_project_id, :integer - end -end diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb new file mode 100644 index 0000000000..cd44980671 --- /dev/null +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb @@ -0,0 +1,10 @@ +class MigrateProjectIdForCiCommits < ActiveRecord::Migration + def up + execute( + "UPDATE ci_commits " + + "JOIN ci_projects ON ci_projects.id = ci_commits.project_id " + + "SET gl_project_id=ci_projects.gitlab_id " + + "WHERE gl_project_id IS NULL" + ) + end +end diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb deleted file mode 100644 index 28d63c8c84..0000000000 --- a/db/migrate/20150924125436_migrate_project_id_for_ci_tables.rb +++ /dev/null @@ -1,15 +0,0 @@ -class MigrateProjectIdForCiTables < ActiveRecord::Migration - TABLES = %w(ci_builds ci_commits ci_events ci_runner_projects - ci_services ci_triggers ci_variables ci_web_hooks) - - def up - TABLES.each do |table| - execute( - "UPDATE #{table} " + - "JOIN ci_projects ON ci_projects.id = #{table}.project_id " + - "SET gl_project_id=ci_projects.gitlab_id " + - "WHERE gl_project_id IS NULL" - ) - end - end -end diff --git a/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb b/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb deleted file mode 100644 index bb97ec3005..0000000000 --- a/db/migrate/20150924131004_add_ci_fields_to_projects_table.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddCiFieldsToProjectsTable < ActiveRecord::Migration - def up - add_column :projects, :shared_runners_enabled, :boolean, default: false - end -end From 3031209b07c1797fe837b0eb871c56bc1f0276bb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 13:40:09 +0200 Subject: [PATCH 067/543] Fix db/schema.rb --- db/schema.rb | 55 +++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 89aafba511..990035fb59 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,6 +13,9 @@ ActiveRecord::Schema.define(version: 20150924131004) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "abuse_reports", force: true do |t| t.integer "reporter_id" t.integer "user_id" @@ -82,19 +85,19 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.integer "project_id" t.string "status" t.datetime "finished_at" - t.text "trace", limit: 2147483647 + t.text "trace" t.datetime "created_at" t.datetime "updated_at" t.datetime "started_at" t.integer "runner_id" - t.float "coverage", limit: 24 + t.float "coverage" t.integer "commit_id" t.text "commands" t.integer "job_id" t.string "name" - t.boolean "deploy", default: false + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false + t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" t.integer "gl_project_id" @@ -110,10 +113,10 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.string "ref" t.string "sha" t.string "before_sha" - t.text "push_data", limit: 16777215 + t.text "push_data" t.datetime "created_at" t.datetime "updated_at" - t.boolean "tag", default: false + t.boolean "tag", default: false t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" @@ -430,9 +433,9 @@ ActiveRecord::Schema.define(version: 20150924131004) do create_table "merge_request_diffs", force: true do |t| t.string "state" - t.text "st_commits", limit: 2147483647 - t.text "st_diffs", limit: 2147483647 - t.integer "merge_request_id", null: false + t.text "st_commits" + t.text "st_diffs" + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -514,8 +517,8 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false - t.text "st_diff", limit: 2147483647 + t.boolean "system", default: false, null: false + t.text "st_diff" t.integer "updated_by_id" end @@ -584,26 +587,26 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", limit: 24, default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 - t.boolean "shared_runners_enabled", default: false + t.integer "commit_count", default: 0 + t.boolean "shared_runners_enabled", default: false end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -655,15 +658,15 @@ ActiveRecord::Schema.define(version: 20150924131004) do create_table "snippets", force: true do |t| t.string "title" - t.text "content", limit: 2147483647 - t.integer "author_id", null: false + t.text "content" + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" t.string "type" - t.integer "visibility_level", default: 0, null: false + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree From 6abca1284791475a8240e4b25b5e6e7175533101 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 13:41:21 +0200 Subject: [PATCH 068/543] Revert check_enable_flag! changes --- app/controllers/ci/application_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index da77e2b94e..d8227e632e 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -11,10 +11,10 @@ module Ci private def check_enable_flag! - # unless current_application_settings.ci_enabled - # redirect_to(disabled_ci_projects_path) - # return - # end + unless current_application_settings.ci_enabled + redirect_to(disabled_ci_projects_path) + return + end end def authenticate_public_page! From 2c1f7ccac8180a49e45fb3cf79e03318420d1037 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 14:44:07 +0200 Subject: [PATCH 069/543] Fix register_build_service tests --- app/models/ci/runner.rb | 4 ++++ app/services/ci/register_build_service.rb | 6 +++--- spec/services/ci/register_build_service_spec.rb | 9 ++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 1e9f78a374..6838ccfaaa 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -41,6 +41,10 @@ module Ci query: "%#{query.try(:downcase)}%") end + def gl_projects_ids + projects.select(:gitlab_id) + end + def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 33f1c1e918..78cc51d31b 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,10 +8,10 @@ module Ci builds = if current_runner.shared? # don't run projects which have not enables shared runners - builds.includes(:project).where(ci_projects: { shared_runners_enabled: true }) + builds.joins(commit: {gl_project: :gitlab_ci_project}).where(ci_projects: {shared_runners_enabled: true}) else # do run projects which are only assigned to this runner - builds.where(project_id: current_runner.projects) + builds.joins(:commit).where(ci_commits: {gl_project_id: current_runner.gl_projects_ids}) end builds = builds.order('created_at ASC') @@ -19,7 +19,7 @@ module Ci build = builds.find do |build| (build.tag_list - current_runner.tag_list).empty? end - + if build # In case when 2 runners try to assign the same build, second runner will be declined diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 9057791ca4..ae4239be82 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -3,15 +3,15 @@ require 'spec_helper' module Ci describe RegisterBuildService do let!(:service) { RegisterBuildService.new } - let!(:project) { FactoryGirl.create :ci_project } - let!(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let!(:gl_project) { FactoryGirl.create :empty_project } let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } before do - specific_runner.assign_to(project) + gl_project.ensure_ci_project + specific_runner.assign_to(gl_project.gitlab_ci_project) end describe :execute do @@ -48,8 +48,7 @@ module Ci context 'allow shared runners' do before do - project.shared_runners_enabled = true - project.save + gl_project.gitlab_ci_project.update(shared_runners_enabled: true) end context 'shared runner' do From 8f8efcfa00c40beacae9886d7be51e82e7a67989 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 15:18:25 +0200 Subject: [PATCH 070/543] Fix tests --- app/models/ci/project.rb | 6 +++--- app/models/project.rb | 1 - app/views/ci/admin/projects/_project.html.haml | 2 +- spec/models/ci/project_spec.rb | 11 ++++------- spec/requests/ci/builds_spec.rb | 2 +- spec/requests/ci/commits_spec.rb | 2 +- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 89bbbea5c5..ba6e320426 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -48,7 +48,7 @@ module Ci accepts_nested_attributes_for :variables, allow_destroy: true - delegate :commits, :builds, :last_commit, to: :gl_project + delegate :commits, :builds, to: :gl_project # # Validations @@ -103,8 +103,8 @@ module Ci end def ordered_by_last_commit_date - last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)" - joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id"). + 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"). order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") end diff --git a/app/models/project.rb b/app/models/project.rb index ddf8526d6c..a5393d396f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -120,7 +120,6 @@ class Project < ActiveRecord::Base has_many :starrers, through: :users_star_projects, source: :user has_many :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 :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' - has_one :last_commit, -> { order 'ci_commits.created_at DESC' }, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml index c461206c72..a342d6e1cf 100644 --- a/app/views/ci/admin/projects/_project.html.haml +++ b/app/views/ci/admin/projects/_project.html.haml @@ -1,4 +1,4 @@ -- last_commit = project.last_commit +- last_commit = project.commits.last %tr %td = project.id diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index b83eccb94e..fe12160659 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -28,9 +28,8 @@ require 'spec_helper' describe Ci::Project do - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let (:gl_project) { } - subject { FactoryGirl.build :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project } + subject { FactoryGirl.create :ci_project, gl_project: gl_project } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } @@ -40,9 +39,7 @@ describe Ci::Project do it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:services) } - it { is_expected.to validate_presence_of :name } it { is_expected.to validate_presence_of :timeout } - it { is_expected.to validate_presence_of :default_ref } describe 'before_validation' do it 'should set an random token if none provided' do @@ -78,7 +75,7 @@ describe Ci::Project do 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.commits).to eq([commit2.project, commit1.project]) + expect(project.commits).to eq([commit2, commit1]) end it 'returns commits ordered by committed_at and id, with nulls last' do @@ -86,7 +83,7 @@ describe Ci::Project do 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.commits).to eq([commit2.project, commit4.project, commit3.project, commit1.project]) + expect(project.commits).to eq([commit2, commit4, commit3, commit1]) end end diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb index 32ca52686a..f68116c52a 100644 --- a/spec/requests/ci/builds_spec.rb +++ b/spec/requests/ci/builds_spec.rb @@ -8,7 +8,7 @@ describe "Builds" do describe "GET /:project/builds/:id/status.json" do before do - get status_ci_project_build_path(@project, @build), format: :json + get status_ci_project_build_path(@commit.project, @build), format: :json end it { expect(response.status).to eq(200) } diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb index 4d7f132023..3ab8c915df 100644 --- a/spec/requests/ci/commits_spec.rb +++ b/spec/requests/ci/commits_spec.rb @@ -7,7 +7,7 @@ describe "Commits" do describe "GET /:project/refs/:ref_name/commits/:id/status.json" do before do - get status_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), format: :json + get status_ci_project_ref_commits_path(@commit.project, @commit.ref, @commit.sha), format: :json end it { expect(response.status).to eq(200) } From e60647f011e30c215dfe596e079c616545071732 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 15:20:08 +0200 Subject: [PATCH 071/543] Fix rubocop --- app/services/ci/register_build_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 78cc51d31b..71b61bbe38 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,10 +8,10 @@ module Ci builds = if current_runner.shared? # don't run projects which have not enables shared runners - builds.joins(commit: {gl_project: :gitlab_ci_project}).where(ci_projects: {shared_runners_enabled: true}) + builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true }) else # do run projects which are only assigned to this runner - builds.joins(:commit).where(ci_commits: {gl_project_id: current_runner.gl_projects_ids}) + builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids }) end builds = builds.order('created_at ASC') From 0fa4ab5fd8e0cb775ff4fe75d011ff717f0b3945 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 15:33:19 +0200 Subject: [PATCH 072/543] Rename commits to ci_commits --- app/models/ci/project.rb | 10 ++++++++-- app/models/project.rb | 4 ++-- spec/models/ci/project_spec.rb | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index ba6e320426..77cce261fc 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -48,8 +48,6 @@ module Ci accepts_nested_attributes_for :variables, allow_destroy: true - delegate :commits, :builds, to: :gl_project - # # Validations # @@ -210,5 +208,13 @@ module Ci def setup_finished? commits.any? end + + def commits + gl_project.ci_commits + end + + def builds + gl_project.ci_builds + end end end diff --git a/app/models/project.rb b/app/models/project.rb index a5393d396f..d9334adef7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -118,8 +118,8 @@ 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 :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 :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' + 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_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index fe12160659..6ccd399e07 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -75,7 +75,7 @@ describe Ci::Project do 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.commits).to eq([commit2, commit1]) + expect(project.ci_commits).to eq([commit2, commit1]) end it 'returns commits ordered by committed_at and id, with nulls last' do @@ -83,7 +83,7 @@ describe Ci::Project do 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.commits).to eq([commit2, commit4, commit3, commit1]) + expect(project.ci_commits).to eq([commit2, commit4, commit3, commit1]) end end From 0f3deac362cf2800ceaf17f4cded765f6c9d577c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 16:49:31 +0200 Subject: [PATCH 073/543] Fix tests --- features/steps/project/commits/commits.rb | 2 +- features/steps/shared/project.rb | 2 +- spec/lib/ci/charts_spec.rb | 5 ++--- spec/models/project_spec.rb | 8 +++++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 56f1f06fb0..47f58091b9 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -104,7 +104,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci(@user) - create :ci_commit, project: @project.gitlab_ci_project, sha: sample_commit.id + create :ci_commit, gl_project: @project, sha: sample_commit.id end step 'I see commit ci info' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index fa841f6751..fc51cec150 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -204,6 +204,6 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, project: project.gitlab_ci_project, sha: project.commit.sha + create :ci_commit, gl_project: project, sha: project.commit.sha end end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 24894e8198..83e2ad220b 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -4,13 +4,12 @@ describe "Charts" do context "build_times" do before do - @project = FactoryGirl.create(:ci_project) - @commit = FactoryGirl.create(:ci_commit, project: @project) + @commit = FactoryGirl.create(:ci_commit) FactoryGirl.create(:ci_build, commit: @commit) end it 'should return build times in minutes' do - chart = Ci::Charts::BuildTime.new(@project) + chart = Ci::Charts::BuildTime.new(@commit.project) expect(chart.build_times).to eq([2]) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9e7b6f5cb3..cbb49044cd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -404,10 +404,12 @@ describe Project do describe :ci_commit do let(:project) { create :project } - let(:ci_project) { create :ci_project, gl_project: project } - let(:commit) { create :ci_commit, project: ci_project } + let(:commit) { create :ci_commit, gl_project: project } - before { project.create_gitlab_ci_service(active: true) } + before do + project.ensure_ci_project + project.create_gitlab_ci_service(active: true) + end it { expect(project.ci_commit(commit.sha)).to eq(commit) } end From 9498a40052603a0687a0fa6370e50e97bc078301 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 28 Sep 2015 16:49:40 +0200 Subject: [PATCH 074/543] Fix migrations --- ...25436_migrate_project_id_for_ci_commits.rb | 8 ++---- db/schema.rb | 26 +++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb index cd44980671..2be57b6062 100644 --- a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb @@ -1,10 +1,6 @@ class MigrateProjectIdForCiCommits < ActiveRecord::Migration def up - execute( - "UPDATE ci_commits " + - "JOIN ci_projects ON ci_projects.id = ci_commits.project_id " + - "SET gl_project_id=ci_projects.gitlab_id " + - "WHERE gl_project_id IS NULL" - ) + subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id' + execute("UPDATE ci_commits SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL") end end diff --git a/db/schema.rb b/db/schema.rb index 990035fb59..0302da6659 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: 20150924131004) do +ActiveRecord::Schema.define(version: 20150924125436) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -45,8 +45,8 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" - t.boolean "ci_enabled", default: true, null: false t.text "help_page_text" + t.boolean "ci_enabled", default: true, null: false end create_table "audit_events", force: true do |t| @@ -100,7 +100,6 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" - t.integer "gl_project_id" end add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree @@ -135,7 +134,6 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree @@ -183,11 +181,10 @@ ActiveRecord::Schema.define(version: 20150924131004) do end create_table "ci_runner_projects", force: true do |t| - t.integer "runner_id", null: false - t.integer "project_id", null: false + t.integer "runner_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree @@ -211,12 +208,11 @@ ActiveRecord::Schema.define(version: 20150924131004) do create_table "ci_services", force: true do |t| t.string "type" t.string "title" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.integer "gl_project_id" end add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree @@ -261,11 +257,10 @@ ActiveRecord::Schema.define(version: 20150924131004) do create_table "ci_triggers", force: true do |t| t.string "token" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree @@ -277,17 +272,15 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.text "encrypted_value" t.string "encrypted_value_salt" t.string "encrypted_value_iv" - t.integer "gl_project_id" end add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree create_table "ci_web_hooks", force: true do |t| - t.string "url", null: false - t.integer "project_id", null: false + t.string "url", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end create_table "deploy_keys_projects", force: true do |t| @@ -606,7 +599,6 @@ ActiveRecord::Schema.define(version: 20150924131004) do t.string "import_type" t.string "import_source" t.integer "commit_count", default: 0 - t.boolean "shared_runners_enabled", default: false end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree From 0d877d91e7556edfcdc29ad77491740da3cc7661 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 29 Sep 2015 10:44:53 +0200 Subject: [PATCH 075/543] Make ensure_gitlab_ci_project return ci_project or create a new one --- app/models/ci/commit.rb | 6 +----- app/models/project.rb | 6 ++---- spec/models/ci/project_spec.rb | 8 ++++---- spec/models/project_spec.rb | 2 +- spec/services/ci/register_build_service_spec.rb | 3 +-- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index a6556690b9..6d048779cd 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -37,11 +37,7 @@ module Ci end def project - unless @project - gl_project.ensure_ci_project - @project = gl_project.gitlab_ci_project - end - @project + @project ||= gl_project.ensure_gitlab_ci_project end def project_id diff --git a/app/models/project.rb b/app/models/project.rb index d9334adef7..953b37e3f7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -747,10 +747,8 @@ class Project < ActiveRecord::Base gitlab_ci_project.commits.find_by(sha: sha) if gitlab_ci? end - def ensure_ci_project - unless gitlab_ci_project - create_gitlab_ci_project - end + def ensure_gitlab_ci_project + gitlab_ci_project || create_gitlab_ci_project end def enable_ci(user) diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index 6ccd399e07..466c7afaf1 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -56,16 +56,16 @@ describe Ci::Project do describe "ordered_by_last_commit_date" do it "returns ordered projects" do newest_project = FactoryGirl.create :empty_project - newest_project.ensure_ci_project + newest_ci_project = newest_project.ensure_gitlab_ci_project oldest_project = FactoryGirl.create :empty_project - oldest_project.ensure_ci_project + oldest_ci_project = oldest_project.ensure_gitlab_ci_project project_without_commits = FactoryGirl.create :empty_project - project_without_commits.ensure_ci_project + ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project - expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_project.gitlab_ci_project, oldest_project.gitlab_ci_project, project_without_commits.gitlab_ci_project]) + expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits]) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cbb49044cd..ba8897b95d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -407,7 +407,7 @@ describe Project do let(:commit) { create :ci_commit, gl_project: project } before do - project.ensure_ci_project + project.ensure_gitlab_ci_project project.create_gitlab_ci_service(active: true) end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index ae4239be82..781764627a 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -10,8 +10,7 @@ module Ci let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } before do - gl_project.ensure_ci_project - specific_runner.assign_to(gl_project.gitlab_ci_project) + specific_runner.assign_to(gl_project.ensure_gitlab_ci_project) end describe :execute do From 3591afeef0da33429b9fd08bbf066120f1653af3 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 29 Sep 2015 13:37:07 +0200 Subject: [PATCH 076/543] Apache needs gitlab-git-http-server on TCP --- doc/update/7.14-to-8.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 86c0825dff..552216be93 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -168,6 +168,7 @@ git diff origin/7-14-stable:lib/support/nginx/gitlab origin/8-0-stable:lib/suppo ``` 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 From 87240e989ba913bad787d8bc81da1a9b87f1da53 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Sep 2015 16:07:44 +0200 Subject: [PATCH 077/543] Move CI project settings page to CE project settings area Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/projects_controller.rb | 33 ++--------------- .../projects/application_controller.rb | 2 +- .../projects/ci_settings_controller.rb | 36 +++++++++++++++++++ app/helpers/ci/gitlab_helper.rb | 4 +-- app/views/layouts/ci/_nav_project.html.haml | 6 ---- .../layouts/nav/_project_settings.html.haml | 5 +++ .../ci_settings}/_form.html.haml | 21 ++++++----- .../ci_settings}/edit.html.haml | 8 ++--- config/routes.rb | 1 + spec/features/ci/projects_spec.rb | 18 ---------- spec/features/ci_settings_spec.rb | 22 ++++++++++++ 12 files changed, 86 insertions(+), 71 deletions(-) create mode 100644 app/controllers/projects/ci_settings_controller.rb rename app/views/{ci/projects => projects/ci_settings}/_form.html.haml (83%) rename app/views/{ci/projects => projects/ci_settings}/edit.html.haml (77%) create mode 100644 spec/features/ci_settings_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 458758b320..a499dda2bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.1.0 (unreleased) - Move CI runners page to project settings area - Move CI variables page to project settings area - Move CI triggers page to project settings area + - Move CI project settings page to CE project settings area v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 13766fb8f6..e8788955eb 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -2,9 +2,9 @@ module Ci class ProjectsController < Ci::ApplicationController before_action :authenticate_user!, except: [:build, :badge, :show] before_action :authenticate_public_page!, only: :show - before_action :project, only: [:build, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml] before_action :authorize_access_project!, except: [:build, :badge, :show, :new, :disabled] - before_action :authorize_manage_project!, only: [:edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] before_action :authenticate_token!, only: [:build] before_action :no_cache, only: [:badge] skip_before_action :check_enable_flag!, only: [:disabled] @@ -23,28 +23,6 @@ module Ci @commits = @commits.page(params[:page]).per(20) end - def edit - end - - def update - if project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, project) - - redirect_to :back, notice: 'Project was successfully updated.' - else - render action: "edit" - end - end - - def destroy - project.gl_project.gitlab_ci_service.update_attributes(active: false) - project.destroy - - Ci::EventService.new.remove_project(current_user, project) - - redirect_to ci_projects_url - end - # Project status badge # Image with build status for sha or ref def badge @@ -74,12 +52,5 @@ module Ci response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end - - def project_params - params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, - :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, - :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, - { variables_attributes: [:id, :key, :value, :_destroy] }) - end end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 56a63ce975..519d6d6127 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -33,6 +33,6 @@ class Projects::ApplicationController < ApplicationController end def ci_project - @ci_project ||= @project.gitlab_ci_project + @ci_project ||= @project.ensure_gitlab_ci_project end end diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb new file mode 100644 index 0000000000..a263242a85 --- /dev/null +++ b/app/controllers/projects/ci_settings_controller.rb @@ -0,0 +1,36 @@ +class Projects::CiSettingsController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout "project_settings" + + def edit + end + + def update + if ci_project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, ci_project) + + redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.' + else + render action: "edit" + end + end + + def destroy + ci_project.destroy + Ci::EventService.new.remove_project(current_user, ci_project) + project.gitlab_ci_service.update_attributes(active: false) + + redirect_to project_path(project), notice: "CI was disabled for this project" + end + + protected + + def project_params + params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, + :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, + :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, + { variables_attributes: [:id, :key, :value, :_destroy] }) + end +end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb index 2b89a0ce93..13e4d0fd9c 100644 --- a/app/helpers/ci/gitlab_helper.rb +++ b/app/helpers/ci/gitlab_helper.rb @@ -27,9 +27,9 @@ module Ci commits = project.commits if commits.any? && commits.last.push_data[:ci_yaml_file] - "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml" + "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" else - "#{@project.gitlab_url}/new/master" + "#{project.gitlab_url}/new/master" end end end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 2d3cc3cf98..3a2741367c 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -26,9 +26,3 @@ = icon('book fw') %span Events - %li.separate-item - = nav_link path: 'projects#edit' do - = link_to edit_ci_project_path(@project) do - = icon('cogs fw') - %span - Settings diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 28efb035d0..26cccb48f6 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -50,3 +50,8 @@ = icon('retweet fw') %span Triggers + = nav_link path: 'ci_settings#edit' do + = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do + = icon('building fw') + %span + CI Settings diff --git a/app/views/ci/projects/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml similarity index 83% rename from app/views/ci/projects/_form.html.haml rename to app/views/projects/ci_settings/_form.html.haml index e782fd8a0f..9f891f557a 100644 --- a/app/views/ci/projects/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -1,17 +1,20 @@ +%h3.page-title + CI settings +%hr .bs-callout.help-callout %p If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} %p - Edit your - #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)} + Edit your + #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} -= nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f| - - if @project.errors.any? += nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| + - if @ci_project.errors.any? #error_explanation - %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" .alert.alert-error %ul - - @project.errors.full_messages.each do |msg| + - @ci_project.errors.full_messages.each do |msg| %li= msg %fieldset @@ -93,8 +96,8 @@ = f.label :token, "CI token", class: 'control-label' .col-sm-10 = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' - + .form-actions = f.submit 'Save changes', class: 'btn btn-save' - - unless @project.new_record? - = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' + - unless @ci_project.new_record? + = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/ci/projects/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml similarity index 77% rename from app/views/ci/projects/edit.html.haml rename to app/views/projects/ci_settings/edit.html.haml index 876ae5182d..e9040fe433 100644 --- a/app/views/ci/projects/edit.html.haml +++ b/app/views/projects/ci_settings/edit.html.haml @@ -1,6 +1,6 @@ -- if @project.generated_yaml_config +- 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(@project)} + CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)} or %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview yaml file which is based on your old jobs. @@ -8,7 +8,7 @@ = render 'form' -- if @project.generated_yaml_config +- if @ci_project.generated_yaml_config #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} .modal-dialog .modal-content @@ -16,6 +16,6 @@ %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × %h4.modal-title Content of .gitlab-ci.yml .modal-body - = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control" + = text_area_tag :yaml, @ci_project.generated_yaml_config, size: "70x25", class: "form-control" .modal-footer %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/config/routes.rb b/config/routes.rb index f7317fb5d9..6d96d8801c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -590,6 +590,7 @@ Gitlab::Application.routes.draw do 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] + resource :ci_settings, only: [:edit, :update, :destroy] resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb index 7c8cd1ce5c..c633acf85f 100644 --- a/spec/features/ci/projects_spec.rb +++ b/spec/features/ci/projects_spec.rb @@ -17,22 +17,4 @@ describe "Projects" do it { expect(page).to have_content @project.name } it { expect(page).to have_content 'All commits' } end - - describe "GET /ci/projects/:id/edit" do - before do - visit edit_ci_project_path(@project) - end - - it { expect(page).to have_content @project.name } - it { expect(page).to have_content 'Build Schedule' } - - it "updates configuration" do - fill_in 'Timeout', with: '70' - click_button 'Save changes' - - expect(page).to have_content 'was successfully updated' - - expect(find_field('Timeout').value).to eq '70' - end - end end diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb new file mode 100644 index 0000000000..7e25e88301 --- /dev/null +++ b/spec/features/ci_settings_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "CI settings" do + let(:user) { create(:user) } + before { login_as(user) } + + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project) + end + + it { expect(page).to have_content 'Build Schedule' } + + it "updates configuration" do + fill_in 'Timeout', with: '70' + click_button 'Save changes' + expect(page).to have_content 'was successfully updated' + expect(find_field('Timeout').value).to eq '70' + end +end From 3d9a7cdb4be6d6d059dfd7ccc8fa2e4bd3ab12e3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 29 Sep 2015 16:26:40 +0200 Subject: [PATCH 078/543] Refactor CI helpers Signed-off-by: Dmitriy Zaporozhets --- app/helpers/builds_helper.rb | 17 ++++++ app/helpers/ci/application_helper.rb | 54 ------------------- app/helpers/ci/builds_helper.rb | 19 ------- app/helpers/ci/icons_helper.rb | 11 ---- app/helpers/ci/runners_helper.rb | 22 -------- app/helpers/ci/triggers_helper.rb | 7 --- app/helpers/ci/user_helper.rb | 15 ------ app/helpers/ci_status_helper.rb | 36 ++++++++----- app/helpers/runners_helper.rb | 20 +++++++ app/helpers/time_helper.rb | 27 ++++++++++ app/helpers/triggers_helper.rb | 5 ++ spec/helpers/{ci => }/runners_helper_spec.rb | 2 +- ...ion_helper_spec.rb => time_helper_spec.rb} | 2 +- 13 files changed, 94 insertions(+), 143 deletions(-) create mode 100644 app/helpers/builds_helper.rb delete mode 100644 app/helpers/ci/application_helper.rb delete mode 100644 app/helpers/ci/builds_helper.rb delete mode 100644 app/helpers/ci/icons_helper.rb delete mode 100644 app/helpers/ci/runners_helper.rb delete mode 100644 app/helpers/ci/triggers_helper.rb delete mode 100644 app/helpers/ci/user_helper.rb create mode 100644 app/helpers/runners_helper.rb create mode 100644 app/helpers/time_helper.rb create mode 100644 app/helpers/triggers_helper.rb rename spec/helpers/{ci => }/runners_helper_spec.rb (94%) rename spec/helpers/{ci/application_helper_spec.rb => time_helper_spec.rb} (96%) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb new file mode 100644 index 0000000000..b6658e52d0 --- /dev/null +++ b/app/helpers/builds_helper.rb @@ -0,0 +1,17 @@ +module BuildsHelper + def build_ref_link build + gitlab_ref_link build.project, build.ref + end + + def build_compare_link build + gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha + end + + def build_commit_link build + gitlab_commit_link build.project, build.short_sha + end + + def build_url(build) + ci_project_build_url(build.project, build) + end +end diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb deleted file mode 100644 index 9fe6282bb8..0000000000 --- a/app/helpers/ci/application_helper.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Ci - module ApplicationHelper - def loader_html - image_tag 'ci/loader.gif', alt: 'Loading' - end - - def date_from_to(from, to) - "#{from.to_s(:short)} - #{to.to_s(:short)}" - end - - def duration_in_words(finished_at, started_at) - if finished_at && started_at - interval_in_seconds = finished_at.to_i - started_at.to_i - elsif started_at - interval_in_seconds = Time.now.to_i - started_at.to_i - end - - time_interval_in_words(interval_in_seconds) - end - - def time_interval_in_words(interval_in_seconds) - minutes = interval_in_seconds / 60 - seconds = interval_in_seconds - minutes * 60 - - if minutes >= 1 - "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" - else - "#{pluralize(seconds, "second")}" - end - end - - def ci_icon_for_status(status) - icon_name = - case status - when 'success' - 'check-square' - when 'failed' - 'close' - when 'running', 'pending' - 'clock-o' - else - 'circle' - end - - icon(icon_name) - end - - def ci_status_with_icon(status) - content_tag :span, class: "ci-status ci-#{status}" do - ci_icon_for_status(status) + ' '.html_safe + status - end - end - end -end diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb deleted file mode 100644 index 5d6e785d95..0000000000 --- a/app/helpers/ci/builds_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - module BuildsHelper - def build_ref_link build - gitlab_ref_link build.project, build.ref - end - - def build_compare_link build - gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha - end - - def build_commit_link build - gitlab_commit_link build.project, build.short_sha - end - - def build_url(build) - ci_project_build_url(build.project, build) - end - end -end diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb deleted file mode 100644 index be40f79e88..0000000000 --- a/app/helpers/ci/icons_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Ci - module IconsHelper - def boolean_to_icon(value) - if value.to_s == "true" - content_tag :i, nil, class: 'fa fa-circle cgreen' - else - content_tag :i, nil, class: 'fa fa-power-off clgray' - end - end - end -end diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb deleted file mode 100644 index 03c9914641..0000000000 --- a/app/helpers/ci/runners_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Ci - 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" - 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 -end diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb deleted file mode 100644 index 0d2438928c..0000000000 --- a/app/helpers/ci/triggers_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - module TriggersHelper - def ci_build_trigger_url(project_id, ref_name) - "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" - end - end -end diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb deleted file mode 100644 index c332d6ed9c..0000000000 --- a/app/helpers/ci/user_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Ci - module UserHelper - def user_avatar_url(user = nil, size = nil, default = 'identicon') - size = 40 if size.nil? || size <= 0 - - if user.blank? || user.avatar_url.blank? - 'ci/no_avatar.png' - elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url - Regexp.last_match[0] + "?s=#{size}&d=#{default}" - else - user.avatar_url - end - end - end -end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 18c30ddb28..3a88ed7107 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,19 +4,7 @@ module CiStatusHelper end def ci_status_icon(ci_commit) - icon_name = - case ci_commit.status - when 'success' - 'check' - when 'failed' - 'close' - when 'running', 'pending' - 'clock-o' - else - 'circle' - end - - icon(icon_name) + ci_icon_for_status(ci_commit.status) end def ci_status_color(ci_commit) @@ -31,4 +19,26 @@ module CiStatusHelper 'gray' end end + + def ci_status_with_icon(status) + content_tag :span, class: "ci-status ci-#{status}" do + ci_icon_for_status(status) + ' '.html_safe + status + end + end + + def ci_icon_for_status(status) + icon_name = + case status + when 'success' + 'check' + when 'failed' + 'close' + when 'running', 'pending' + 'clock-o' + else + 'circle' + end + + icon(icon_name) + end end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb new file mode 100644 index 0000000000..5d7d06c849 --- /dev/null +++ b/app/helpers/runners_helper.rb @@ -0,0 +1,20 @@ +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" + 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/helpers/time_helper.rb b/app/helpers/time_helper.rb new file mode 100644 index 0000000000..8142f733e7 --- /dev/null +++ b/app/helpers/time_helper.rb @@ -0,0 +1,27 @@ +module TimeHelper + def duration_in_words(finished_at, started_at) + if finished_at && started_at + interval_in_seconds = finished_at.to_i - started_at.to_i + elsif started_at + interval_in_seconds = Time.now.to_i - started_at.to_i + end + + time_interval_in_words(interval_in_seconds) + end + + def time_interval_in_words(interval_in_seconds) + minutes = interval_in_seconds / 60 + seconds = interval_in_seconds - minutes * 60 + + if minutes >= 1 + "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + else + "#{pluralize(seconds, "second")}" + end + end + + + def date_from_to(from, to) + "#{from.to_s(:short)} - #{to.to_s(:short)}" + end +end diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb new file mode 100644 index 0000000000..2a3a7e80fc --- /dev/null +++ b/app/helpers/triggers_helper.rb @@ -0,0 +1,5 @@ +module TriggersHelper + def ci_build_trigger_url(project_id, ref_name) + "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + end +end diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb similarity index 94% rename from spec/helpers/ci/runners_helper_spec.rb rename to spec/helpers/runners_helper_spec.rb index 6d0e2d3d1e..b3d635a193 100644 --- a/spec/helpers/ci/runners_helper_spec.rb +++ b/spec/helpers/runners_helper_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::RunnersHelper do +describe RunnersHelper do it "returns - not contacted yet" do runner = FactoryGirl.build :ci_runner expect(runner_status_icon(runner)).to include("not connected yet") diff --git a/spec/helpers/ci/application_helper_spec.rb b/spec/helpers/time_helper_spec.rb similarity index 96% rename from spec/helpers/ci/application_helper_spec.rb rename to spec/helpers/time_helper_spec.rb index 6a216715b7..3f62527c5b 100644 --- a/spec/helpers/ci/application_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::ApplicationHelper do +describe TimeHelper do describe "#duration_in_words" do it "returns minutes and seconds" do intervals_in_words = { From 70460f7989739fe06a0c2fbef8dc35bd31e18322 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 29 Sep 2015 17:20:14 +0200 Subject: [PATCH 079/543] Use Golang 1.5.1 for new installs from source --- doc/install/installation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 039bb3c256..6d3b318737 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -131,11 +131,11 @@ Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server. This is a small daemon written in Go. To install gitlab-git-http-server we need a Go compiler. - 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 + curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz + echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \ + sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.5.linux-amd64.tar.gz + rm go1.5.1.linux-amd64.tar.gz ## 4. System Users From 60b3055c0c3061ed0eb2ff40913b6b9d85c6cbae Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 29 Sep 2015 17:24:22 +0200 Subject: [PATCH 080/543] Use Ruby 2.1.7 for new installs from source --- doc/install/installation.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 6d3b318737..518b914fe6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -115,8 +115,9 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz - cd ruby-2.1.6 + curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz + echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz + cd ruby-2.1.7 ./configure --disable-install-rdoc make sudo make install From 5f95a5e070c76c582a2b394377b0f350f4b1cff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 28 Sep 2015 16:00:53 +0200 Subject: [PATCH 081/543] Disable the "Report abuse" button if a user has already been reported --- app/helpers/user_helper.rb | 11 ++++++++++ app/models/user.rb | 2 ++ app/views/users/show.html.haml | 18 ++++++++++------ features/abuse_report.feature | 7 ++++++ features/steps/abuse_reports.rb | 5 +++++ spec/features/users_spec.rb | 38 +++++++++++++++++++++++++++++++-- spec/models/user_spec.rb | 1 + 7 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 app/helpers/user_helper.rb diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb new file mode 100644 index 0000000000..058cff8513 --- /dev/null +++ b/app/helpers/user_helper.rb @@ -0,0 +1,11 @@ +module UserHelper + + def abuse_report_button_title(user) + if user.abuse_report + "#{user.username} has already been reported for abuse." + else + "Report #{user.username} for abuse." + end + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index 3879f3fd38..a3b149ff99 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -97,7 +97,9 @@ class User < ActiveRecord::Base # Namespace for personal projects has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace" + # Profile + has_one :abuse_report, dependent: :destroy has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 37d5dba033..0661d8d06a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -19,14 +19,18 @@ = icon('user') Profile settings - elsif current_user - .pull-right + #report_abuse.pull-right %span.dropdown - %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} - = icon('exclamation-circle') - %ul.dropdown-menu.dropdown-menu-right - %li - = link_to new_abuse_report_path(user_id: @user.id) do - Report abuse + - if @user.abuse_report + %span.light.dropdown-toggle.btn.btn-sm.btn-close{title: abuse_report_button_title(@user)} + = icon('exclamation-circle') + - else + %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} + = icon('exclamation-circle') + %ul.dropdown-menu.dropdown-menu-right + %li + = link_to new_abuse_report_path(user_id: @user.id), title: abuse_report_button_title(@user) do + Report abuse .username @#{@user.username} diff --git a/features/abuse_report.feature b/features/abuse_report.feature index 3e1cb455b7..fc80f0aa39 100644 --- a/features/abuse_report.feature +++ b/features/abuse_report.feature @@ -8,3 +8,10 @@ Feature: Abuse reports And I click "Report abuse" button When I fill and submit abuse form Then I should see success message + + Scenario: Report abuse available only once + Given I visit "Mike" user page + And I click "Report abuse" button + When I fill and submit abuse form + And I visit "Mike" user page + Then I should not see the "Remove abuse" dropdown / button diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb index 8f9ddb2899..623807dac8 100644 --- a/features/steps/abuse_reports.rb +++ b/features/steps/abuse_reports.rb @@ -22,6 +22,11 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps user_mike end + step 'I should not see the "Remove abuse" dropdown / button' do + expect(find(:css, '#report_abuse')).not_to have_selector(:css, 'ul.dropdown-menu') + expect(find(:css, '#report_abuse')).to have_selector(:css, '.btn-close') + end + def user_mike @user_mike ||= create(:user, name: 'Mike') end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index efcb8a31ab..633616241f 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' feature 'Users', feature: true do + let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } + let(:user2) { create(:user, username: 'user2', name: 'User 2', email: 'user2@gitlab.com') } + scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path fill_in 'user_name', with: 'Name Surname' @@ -11,7 +14,6 @@ feature 'Users', feature: true do end scenario 'Successful user signin invalidates password reset token' do - user = create(:user) expect(user.reset_password_token).to be_nil visit new_user_password_path @@ -28,7 +30,6 @@ feature 'Users', feature: true do expect(user.reset_password_token).to be_nil end - let!(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } scenario 'Should show one error if email is already taken' do visit new_user_session_path fill_in 'user_name', with: 'Another user name' @@ -48,4 +49,37 @@ feature 'Users', feature: true do page.find('#error_explanation').find('ul').all('li').count end + context 'With a logged-in user' do + before do + login_as(user) + end + + describe 'Abuse report button' do + context 'User has never been reported for abuse' do + it 'enables the "Report abuse" button / dropdown' do + visit user_path(user2) + + expect(page.find('#report_abuse').find('ul.dropdown-menu').all('li').count).to be(1) + expect(page.find('#report_abuse').all('.btn-close').count).to be(0) + end + end + + context 'User has already been reported for abuse' do + before do + @abuse_report = AbuseReport.new(user: user2, message: 'Foo bar') + @abuse_report.reporter = user + @abuse_report.save! + end + + it 'disables the "Report abuse" button' do + visit user_path(user2) + + expect(page.find('#report_abuse').all('ul.dropdown-menu').count).to be(0) + expect(page.find('#report_abuse').all('.btn-close').count).to be(1) + end + end + end + + end + end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 480950859a..6342c3b8d1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -73,6 +73,7 @@ describe User do describe 'associations' do it { is_expected.to have_one(:namespace) } + it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } From ea72d53ec083676ee1171e97c0869132f360d0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 29 Sep 2015 18:08:55 +0200 Subject: [PATCH 082/543] Streamline the "Report button" This simplifies the "Report button" to not use open a dropdown and adds a tooltip on this button. This also removes an extra spec and adds missing specs. --- app/helpers/user_helper.rb | 11 ---------- app/models/abuse_report.rb | 4 ++-- app/models/user.rb | 3 +-- app/views/users/show.html.haml | 19 +++++++---------- features/abuse_report.feature | 2 +- features/steps/abuse_reports.rb | 5 ++--- spec/features/users_spec.rb | 36 +------------------------------- spec/models/abuse_report_spec.rb | 12 +++++++++++ spec/models/user_spec.rb | 2 +- 9 files changed, 27 insertions(+), 67 deletions(-) delete mode 100644 app/helpers/user_helper.rb diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb deleted file mode 100644 index 058cff8513..0000000000 --- a/app/helpers/user_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module UserHelper - - def abuse_report_button_title(user) - if user.abuse_report - "#{user.username} has already been reported for abuse." - else - "Report #{user.username} for abuse." - end - end - -end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 07c87a7fe8..89b3116b9f 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -11,11 +11,11 @@ # class AbuseReport < ActiveRecord::Base - belongs_to :reporter, class_name: "User" + belongs_to :reporter, class_name: 'User' belongs_to :user validates :reporter, presence: true validates :user, presence: true validates :message, presence: true - validates :user_id, uniqueness: { scope: :reporter_id } + validates :user_id, uniqueness: true end diff --git a/app/models/user.rb b/app/models/user.rb index a3b149ff99..9ea7cabff1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -97,9 +97,7 @@ class User < ActiveRecord::Base # Namespace for personal projects has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace" - # Profile - has_one :abuse_report, dependent: :destroy has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true @@ -131,6 +129,7 @@ class User < ActiveRecord::Base has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy + has_one :abuse_report, dependent: :destroy # diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0661d8d06a..11beb3e323 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -19,18 +19,13 @@ = icon('user') Profile settings - elsif current_user - #report_abuse.pull-right - %span.dropdown - - if @user.abuse_report - %span.light.dropdown-toggle.btn.btn-sm.btn-close{title: abuse_report_button_title(@user)} - = icon('exclamation-circle') - - else - %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} - = icon('exclamation-circle') - %ul.dropdown-menu.dropdown-menu-right - %li - = link_to new_abuse_report_path(user_id: @user.id), title: abuse_report_button_title(@user) do - Report abuse + .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} diff --git a/features/abuse_report.feature b/features/abuse_report.feature index fc80f0aa39..212972a762 100644 --- a/features/abuse_report.feature +++ b/features/abuse_report.feature @@ -14,4 +14,4 @@ Feature: Abuse reports And I click "Report abuse" button When I fill and submit abuse form And I visit "Mike" user page - Then I should not see the "Remove abuse" dropdown / button + Then I should see a red "Report abuse" button diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb index 623807dac8..56652ff6f0 100644 --- a/features/steps/abuse_reports.rb +++ b/features/steps/abuse_reports.rb @@ -22,9 +22,8 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps user_mike end - step 'I should not see the "Remove abuse" dropdown / button' do - expect(find(:css, '#report_abuse')).not_to have_selector(:css, 'ul.dropdown-menu') - expect(find(:css, '#report_abuse')).to have_selector(:css, '.btn-close') + step 'I should see a red "Report abuse" button' do + expect(find(:css, '.report_abuse')).to have_selector(:css, 'span.btn-close') end def user_mike diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 633616241f..c124816203 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' feature 'Users', feature: true do - let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } - let(:user2) { create(:user, username: 'user2', name: 'User 2', email: 'user2@gitlab.com') } + let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path @@ -49,37 +48,4 @@ feature 'Users', feature: true do page.find('#error_explanation').find('ul').all('li').count end - context 'With a logged-in user' do - before do - login_as(user) - end - - describe 'Abuse report button' do - context 'User has never been reported for abuse' do - it 'enables the "Report abuse" button / dropdown' do - visit user_path(user2) - - expect(page.find('#report_abuse').find('ul.dropdown-menu').all('li').count).to be(1) - expect(page.find('#report_abuse').all('.btn-close').count).to be(0) - end - end - - context 'User has already been reported for abuse' do - before do - @abuse_report = AbuseReport.new(user: user2, message: 'Foo bar') - @abuse_report.reporter = user - @abuse_report.save! - end - - it 'disables the "Report abuse" button' do - visit user_path(user2) - - expect(page.find('#report_abuse').all('ul.dropdown-menu').count).to be(0) - expect(page.find('#report_abuse').all('.btn-close').count).to be(1) - end - end - end - - end - end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 635a6e2518..d45319b25d 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -16,4 +16,16 @@ RSpec.describe AbuseReport, type: :model do subject { create(:abuse_report) } it { expect(subject).to be_valid } + + describe 'associations' do + it { is_expected.to belong_to(:reporter).class_name('User') } + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:reporter) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:message) } + it { is_expected.to validate_uniqueness_of(:user_id) } + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6342c3b8d1..b7b525bfca 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -73,7 +73,6 @@ describe User do describe 'associations' do it { is_expected.to have_one(:namespace) } - it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } @@ -86,6 +85,7 @@ describe User do it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } + it { is_expected.to have_one(:abuse_report) } end describe 'validations' do From e471356f76ca2561cf26fc01d6527c6b90e4bf9e Mon Sep 17 00:00:00 2001 From: Diogo Resende Date: Wed, 30 Sep 2015 08:34:00 +0000 Subject: [PATCH 083/543] Fixes english information paragraph in admin/applications view --- app/views/admin/applications/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index fc921a966f..6ac45a3db1 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -2,7 +2,7 @@ %h3.page-title System OAuth applications %p.light - System OAuth application does not belong to certain user and can be managed only by admins + System OAuth application don't belong to any user and can only be managed by admins %hr %p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success' %table.table.table-striped @@ -20,4 +20,4 @@ %td= application.redirect_uri %td= application.access_tokens.map(&:resource_owner_id).uniq.count %td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link' - %td= render 'delete_form', application: application + %td= render 'delete_form', application: application \ No newline at end of file From f71f165f994bbdc5b98bf610f8306e7cad7f1740 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 30 Sep 2015 11:16:00 +0200 Subject: [PATCH 084/543] Fix CI mailer Signed-off-by: Dmitriy Zaporozhets --- app/mailers/ci/notify.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb index 4462da0d7d..404842cf21 100644 --- a/app/mailers/ci/notify.rb +++ b/app/mailers/ci/notify.rb @@ -2,7 +2,6 @@ module Ci class Notify < ActionMailer::Base include Ci::Emails::Builds - add_template_helper Ci::ApplicationHelper add_template_helper Ci::GitlabHelper default_url_options[:host] = Gitlab.config.gitlab.host From fe7e97320c83df771e027a4d1576986e58065a97 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 30 Sep 2015 12:31:02 +0200 Subject: [PATCH 085/543] Fix bug when removed file was not appearing in merge request diff Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/helpers/diff_helper.rb | 19 +++++++++++++++++++ app/views/projects/diffs/_diffs.html.haml | 7 ++++++- app/views/projects/diffs/_file.html.haml | 22 ++++++++-------------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a499dda2bb..492e4b9aeb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.1.0 (unreleased) - Move CI variables page to project settings area - 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 v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 6ffa1a7121..9d718e13b8 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -167,4 +167,23 @@ module DiffHelper content_tag(:span, commit_id, class: 'monospace'), ].join(' ').html_safe end + + def commit_for_diff(diff) + if diff.deleted_file + @merge_request ? @merge_request.commits.last : @commit.parent_id + else + @commit + end + end + + def diff_file_html_data(project, diff_commit, diff_file) + { + blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, + tree_join(diff_commit.id, diff_file.file_path)) + } + end + + def editable_diff?(diff) + !diff.deleted_file && @merge_request && @merge_request.source_project + end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2f24dc7c90..c5acafa263 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -15,7 +15,12 @@ .files - diff_files.each_with_index do |diff_file, index| - = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project + - diff_commit = commit_for_diff(diff_file.diff) + - blob = project.repository.blob_for_diff(diff_commit, diff_file.diff) + - next unless blob + + = render 'projects/diffs/file', i: index, project: project, + diff_file: diff_file, diff_commit: diff_commit, blob: blob - if @diff_timeout .alert.alert-danger diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 99ee23a1dd..4617b18815 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,24 +1,18 @@ -- blob = project.repository.blob_for_diff(@commit, diff_file.diff) -- return unless blob -- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path)) -.diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }} +.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)}"} - - if diff_file.deleted_file - %span="#{diff_file.old_path} deleted" - - .diff-btn-group - - if @commit.parent_ids.present? - = view_file_btn(@commit.parent_id, diff_file, project) - - elsif diff_file.diff.submodule? + - if diff_file.diff.submodule? %span - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) = submodule_link(submodule_item, @commit.id, project.repository) - else %span - - if diff_file.renamed_file + - if diff_file.deleted_file + = "#{diff_file.old_path} deleted" + - elsif diff_file.renamed_file = "#{diff_file.old_path} renamed to #{diff_file.new_path}" - else = diff_file.new_path + - if diff_file.mode_changed? %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" @@ -28,12 +22,12 @@ %i.fa.fa-comments   - - if @merge_request && @merge_request.source_project + - if editable_diff?(diff_file) = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, after: ' ', from_merge_request_id: @merge_request.id) - = view_file_btn(@commit.id, diff_file, project) + = view_file_btn(diff_commit.id, diff_file, project) .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs From e82b37a6537eb86366fbd1506b6dd371b7afe051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Wed, 30 Sep 2015 13:47:50 +0300 Subject: [PATCH 086/543] Add support of multibyte characters in LDAP UID --- lib/gitlab/ldap/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index cb66fd500f..1ea7751e27 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -14,7 +14,7 @@ module Gitlab # LDAP distinguished name is case-insensitive identity = ::Identity. where(provider: provider). - where('lower(extern_uid) = ?', uid.downcase).last + where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last identity && identity.user end end From b9ccc79cb5d67e356edce3778b6a17def985ed22 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 23 Sep 2015 12:47:29 +0200 Subject: [PATCH 087/543] Delegate ci_project parameters to projects - It delegates name, path, gitlab_url, ssh_url_to_repo - Remove ability to set this parameters using CI API This fixes GitLab project rename, namespace change, repository rename, etc. --- .../ci/admin/runners_controller.rb | 5 +- app/models/ci/project.rb | 40 +++++------ .../project_services/gitlab_ci_service.rb | 6 +- ...095736_add_null_to_name_for_ci_projects.rb | 9 +++ db/schema.rb | 4 +- doc/ci/api/projects.md | 5 -- lib/ci/api/projects.rb | 23 ++---- spec/factories/ci/projects.rb | 12 ---- spec/features/ci/admin/runners_spec.rb | 17 +++-- spec/models/ci/project_spec.rb | 71 ++++++++++++++++--- spec/requests/ci/api/projects_spec.rb | 12 ++-- spec/services/ci/event_service_spec.rb | 6 +- 12 files changed, 117 insertions(+), 93 deletions(-) create mode 100644 db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb index dc3508b49d..9a68add908 100644 --- a/app/controllers/ci/admin/runners_controller.rb +++ b/app/controllers/ci/admin/runners_controller.rb @@ -12,7 +12,10 @@ module Ci def show @builds = @runner.builds.order('id DESC').first(30) @projects = Ci::Project.all - @projects = @projects.search(params[:search]) if params[:search].present? + if params[:search].present? + @gl_projects = ::Project.search(params[:search]) + @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.page(params[:page]).per(30) end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 77cce261fc..f0a8fc703b 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -48,11 +48,12 @@ module Ci accepts_nested_attributes_for :variables, allow_destroy: true + delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project + # # Validations # - validates_presence_of :name, :timeout, :token, :default_ref, - :path, :ssh_url_to_repo, :gitlab_id + validates_presence_of :timeout, :token, :default_ref, :gitlab_id validates_uniqueness_of :gitlab_id @@ -60,8 +61,6 @@ module Ci presence: true, if: ->(project) { project.always_build.present? } - scope :public_only, ->() { where(public: true) } - before_validation :set_default_values class << self @@ -76,12 +75,9 @@ module Ci def parse(project) params = { - name: project.name_with_namespace, - gitlab_id: project.id, - path: project.path_with_namespace, - default_ref: project.default_branch || 'master', - ssh_url_to_repo: project.ssh_url_to_repo, - email_add_pusher: current_application_settings.add_pusher, + gitlab_id: project.id, + default_ref: project.default_branch || 'master', + email_add_pusher: current_application_settings.add_pusher, email_only_broken_builds: current_application_settings.all_broken_builds, } @@ -105,11 +101,18 @@ module Ci joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id"). order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") end + end - def search(query) - where("LOWER(#{Ci::Project.table_name}.name) LIKE :query", - query: "%#{query.try(:downcase)}%") - end + def name + name_with_namespace + end + + def path + path_with_namespace + end + + def gitlab_url + web_url end def any_runners? @@ -123,9 +126,6 @@ module Ci def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? self.default_ref ||= 'master' - self.name ||= gl_project.name_with_namespace - self.path ||= gl_project.path_with_namespace - self.ssh_url_to_repo ||= gl_project.ssh_url_to_repo end def tracked_refs @@ -169,7 +169,7 @@ module Ci # using http and basic auth def repo_url_with_auth auth = "gitlab-ci-token:#{token}@" - url = gitlab_url + ".git" + url = http_url_to_repo + ".git" url.sub(/^https?:\/\//) do |prefix| prefix + auth end @@ -201,10 +201,6 @@ module Ci end end - def gitlab_url - File.join(Gitlab.config.gitlab.url, path) - end - def setup_finished? commits.any? end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 23ab206efb..436d4cfed8 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -70,11 +70,7 @@ class GitlabCiService < CiService def fork_registration(new_project, current_user) params = OpenStruct.new({ id: new_project.id, - name_with_namespace: new_project.name_with_namespace, - path_with_namespace: new_project.path_with_namespace, - web_url: new_project.web_url, - default_branch: new_project.default_branch, - ssh_url_to_repo: new_project.ssh_url_to_repo + default_branch: new_project.default_branch }) ci_project = Ci::Project.find_by!(gitlab_id: project.id) diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb new file mode 100644 index 0000000000..a978fcda3b --- /dev/null +++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb @@ -0,0 +1,9 @@ +class ChangeNameOfCiProjects < ActiveRecord::Migration + def up + change_column_null :ci_projects, :name, true + end + + def down + change_column_null :ci_projects, :name, false + end +end diff --git a/db/schema.rb b/db/schema.rb index 0302da6659..4ce6cee86e 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: 20150924125436) do +ActiveRecord::Schema.define(version: 20150930095736) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -158,7 +158,7 @@ ActiveRecord::Schema.define(version: 20150924125436) do add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree create_table "ci_projects", force: true do |t| - t.string "name", null: false + t.string "name" t.integer "timeout", default: 3600, null: false t.datetime "created_at" t.datetime "updated_at" diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md index 54584db093..5585191e82 100644 --- a/doc/ci/api/projects.md +++ b/doc/ci/api/projects.md @@ -100,8 +100,6 @@ Parameters: * `name` (required) - The name of the project * `gitlab_id` (required) - The ID of the project on the Gitlab instance - * `path` (required) - The gitlab project path - * `ssh_url_to_repo` (required) - The gitlab SSH url to the repo * `default_ref` (optional) - The branch to run on (default to `master`) ### Update Project @@ -114,9 +112,6 @@ authenticated user has access to. Parameters: * `name` - The name of the project - * `gitlab_id` - The ID of the project on the Gitlab instance - * `path` - The gitlab project path - * `ssh_url_to_repo` - The gitlab SSH url to the repo * `default_ref` - The branch to run on (default to `master`) ### Remove Project diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb index 66bcf65e8c..d719ad9e8d 100644 --- a/lib/ci/api/projects.rb +++ b/lib/ci/api/projects.rb @@ -75,23 +75,17 @@ module Ci # Create Gitlab CI project using Gitlab project info # # Parameters: - # name (required) - The name of the project # gitlab_id (required) - The gitlab id of the project - # path (required) - The gitlab project path, ex. randx/six - # ssh_url_to_repo (required) - The gitlab ssh url to the repo # default_ref - The branch to run against (defaults to `master`) # Example Request: # POST /projects post do - required_attributes! [:name, :gitlab_id, :ssh_url_to_repo] + required_attributes! [:gitlab_id] filtered_params = { - name: params[:name], gitlab_id: params[:gitlab_id], # we accept gitlab_url for backward compatibility for a while (added to 7.11) - path: params[:path] || params[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1'), - default_ref: params[:default_ref] || 'master', - ssh_url_to_repo: params[:ssh_url_to_repo] + default_ref: params[:default_ref] || 'master' } project = Ci::Project.new(filtered_params) @@ -109,11 +103,7 @@ module Ci # # Parameters: # id (required) - The ID of a project - # name - The name of the project - # gitlab_id - The gitlab id of the project - # path - The gitlab project path, ex. randx/six - # ssh_url_to_repo - The gitlab ssh url to the repo - # default_ref - The branch to run against (defaults to `master`) + # default_ref - The branch to run against (defaults to `master`) # Example Request: # PUT /projects/:id put ":id" do @@ -121,12 +111,7 @@ module Ci unauthorized! unless can?(current_user, :admin_project, project.gl_project) - attrs = attributes_for_keys [:name, :gitlab_id, :path, :gitlab_url, :default_ref, :ssh_url_to_repo] - - # we accept gitlab_url for backward compatibility for a while (added to 7.11) - if attrs[:gitlab_url] && !attrs[:path] - attrs[:path] = attrs[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1') - end + attrs = attributes_for_keys [:default_ref] if project.update_attributes(attrs) present project, with: Entities::Project diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb index d492fe8209..111e1a8281 100644 --- a/spec/factories/ci/projects.rb +++ b/spec/factories/ci/projects.rb @@ -29,20 +29,8 @@ FactoryGirl.define do factory :ci_project_without_token, class: Ci::Project do - sequence :name do |n| - "GitLab / gitlab-shell#{n}" - end - default_ref 'master' - sequence :path do |n| - "gitlab/gitlab-shell#{n}" - end - - sequence :ssh_url_to_repo do |n| - "git@demo.gitlab.com:gitlab/gitlab-shell#{n}.git" - end - gl_project factory: :empty_project factory :ci_project do diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb index b25121f080..5fb3b93525 100644 --- a/spec/features/ci/admin/runners_spec.rb +++ b/spec/features/ci/admin/runners_spec.rb @@ -2,8 +2,7 @@ require 'spec_helper' describe "Admin Runners" do before do - skip_ci_admin_auth - login_as :user + login_as :admin end describe "Runners page" do @@ -37,8 +36,8 @@ describe "Admin Runners" do let(:runner) { FactoryGirl.create :ci_runner } before do - FactoryGirl.create(:ci_project, name: "foo") - FactoryGirl.create(:ci_project, name: "bar") + @project1 = FactoryGirl.create(:ci_project) + @project2 = FactoryGirl.create(:ci_project) visit ci_admin_runner_path(runner) end @@ -47,19 +46,19 @@ describe "Admin Runners" do end describe 'projects' do - it { expect(page).to have_content("foo") } - it { expect(page).to have_content("bar") } + it { expect(page).to have_content(@project1.name_with_namespace) } + it { expect(page).to have_content(@project2.name_with_namespace) } end describe 'search' do before do search_form = find('#runner-projects-search') - search_form.fill_in 'search', with: 'foo' + search_form.fill_in 'search', with: @project1.gl_project.name search_form.click_button 'Search' end - it { expect(page).to have_content("foo") } - it { expect(page).not_to have_content("bar") } + it { expect(page).to have_content(@project1.name_with_namespace) } + it { expect(page).not_to have_content(@project2.name_with_namespace) } end end end diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb index 466c7afaf1..dec4720a71 100644 --- a/spec/models/ci/project_spec.rb +++ b/spec/models/ci/project_spec.rb @@ -29,7 +29,8 @@ require 'spec_helper' describe Ci::Project do let(:gl_project) { FactoryGirl.create :empty_project } - subject { FactoryGirl.create :ci_project, gl_project: gl_project } + let(:project) { FactoryGirl.create :ci_project, gl_project: gl_project } + subject { project } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } @@ -40,6 +41,7 @@ describe Ci::Project do it { is_expected.to have_many(:services) } it { is_expected.to validate_presence_of :timeout } + it { is_expected.to validate_presence_of :gitlab_id } describe 'before_validation' do it 'should set an random token if none provided' do @@ -53,6 +55,66 @@ describe Ci::Project do end end + describe :name_with_namespace do + subject { project.name_with_namespace } + + it { is_expected.to eq(project.name) } + it { is_expected.to eq(gl_project.name_with_namespace) } + end + + describe :path_with_namespace do + subject { project.path_with_namespace } + + it { is_expected.to eq(project.path) } + it { is_expected.to eq(gl_project.path_with_namespace) } + end + + describe :path_with_namespace do + subject { project.web_url } + + it { is_expected.to eq(gl_project.web_url) } + end + + describe :web_url do + subject { project.web_url } + + it { is_expected.to eq(project.gitlab_url) } + it { is_expected.to eq(gl_project.web_url) } + end + + describe :http_url_to_repo do + subject { project.http_url_to_repo } + + it { is_expected.to eq(gl_project.http_url_to_repo) } + end + + describe :ssh_url_to_repo do + subject { project.ssh_url_to_repo } + + it { is_expected.to eq(gl_project.ssh_url_to_repo) } + end + + describe :commits do + subject { project.commits } + + before do + FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + end + + it { is_expected.to eq(gl_project.ci_commits) } + end + + describe :builds do + subject { project.builds } + + before do + commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + FactoryGirl.create :ci_build, commit: commit + end + + it { is_expected.to eq(gl_project.ci_builds) } + end + describe "ordered_by_last_commit_date" do it "returns ordered projects" do newest_project = FactoryGirl.create :empty_project @@ -174,13 +236,6 @@ describe Ci::Project do it { is_expected.to include(project.gitlab_url[7..-1]) } end - describe :search do - let!(:project) { FactoryGirl.create(:ci_project, name: "foo") } - - it { expect(Ci::Project.search('fo')).to include(project) } - it { expect(Ci::Project.search('bar')).to be_empty } - end - describe :any_runners do it "there are no runners available" do project = FactoryGirl.create(:ci_project) diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb index 409f47fa44..53f7f91cc1 100644 --- a/spec/requests/ci/api/projects_spec.rb +++ b/spec/requests/ci/api/projects_spec.rb @@ -134,7 +134,7 @@ describe Ci::API::API do describe "PUT /projects/:id" do let!(:project) { FactoryGirl.create(:ci_project) } - let!(:project_info) { { name: "An updated name!" } } + let!(:project_info) { { default_ref: "develop" } } before do options.merge!(project_info) @@ -144,7 +144,7 @@ describe Ci::API::API do project.gl_project.team << [user, :master] put ci_api("/projects/#{project.id}"), options expect(response.status).to eq(200) - expect(json_response["name"]).to eq(project_info[:name]) + expect(json_response["default_ref"]).to eq(project_info[:default_ref]) end it "fails to update a non-existing project" do @@ -181,12 +181,10 @@ describe Ci::API::API do end describe "POST /projects" do + let(:gl_project) { FactoryGirl.create :empty_project } let(:project_info) do { - name: "My project", - gitlab_id: 1, - path: "testing/testing", - ssh_url_to_repo: "ssh://example.com/testing/testing.git" + gitlab_id: gl_project.id } end @@ -200,7 +198,7 @@ describe Ci::API::API do it "should create a project with valid data" do post ci_api("/projects"), options expect(response.status).to eq(201) - expect(json_response['name']).to eq(project_info[:name]) + expect(json_response['name']).to eq(gl_project.name_with_namespace) end end diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb index 9b330a90ae..1264e17ff5 100644 --- a/spec/services/ci/event_service_spec.rb +++ b/spec/services/ci/event_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::EventService do - let(:project) { FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" } + let(:project) { FactoryGirl.create :ci_project } let(:user) { double(username: "root", id: 1) } before do @@ -12,7 +12,7 @@ describe Ci::EventService do it "creates event" do Ci::EventService.new.remove_project(user, project) - expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been removed by root") + expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root") end end @@ -20,7 +20,7 @@ describe Ci::EventService do it "creates event" do Ci::EventService.new.create_project(user, project) - expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been created by root") + expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root") end end From ad765353f6f62028c0b5331f90210480127d1551 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 30 Sep 2015 12:51:03 +0200 Subject: [PATCH 088/543] Fix sometimes failing tests --- spec/features/ci/admin/runners_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb index 5fb3b93525..b83744f53a 100644 --- a/spec/features/ci/admin/runners_spec.rb +++ b/spec/features/ci/admin/runners_spec.rb @@ -19,16 +19,16 @@ describe "Admin Runners" do describe 'search' do before do - FactoryGirl.create :ci_runner, description: 'foo' - FactoryGirl.create :ci_runner, description: 'bar' + FactoryGirl.create :ci_runner, description: 'runner-foo' + FactoryGirl.create :ci_runner, description: 'runner-bar' search_form = find('#runners-search') - search_form.fill_in 'search', with: 'foo' + search_form.fill_in 'search', with: 'runner-foo' search_form.click_button 'Search' end - it { expect(page).to have_content("foo") } - it { expect(page).not_to have_content("bar") } + it { expect(page).to have_content("runner-foo") } + it { expect(page).not_to have_content("runner-bar") } end end From f9e5a30d039026616d12b74d9b1d96efed8f9468 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 30 Sep 2015 14:24:39 +0200 Subject: [PATCH 089/543] Update docker guide and add docker-compose.yml --- docker-compose.yml | 2 + docker/.dockerignore | 1 - docker/Dockerfile | 50 ----------- docker/README.md | 172 ++------------------------------------ docker/assets/wrapper | 21 ----- docker/fig.yml | 2 - docker/marathon.json | 31 ------- docker/troubleshooting.md | 84 ------------------- 8 files changed, 7 insertions(+), 356 deletions(-) create mode 100644 docker-compose.yml delete mode 100644 docker/.dockerignore delete mode 100644 docker/Dockerfile delete mode 100755 docker/assets/wrapper delete mode 100644 docker/fig.yml delete mode 100644 docker/marathon.json delete mode 100644 docker/troubleshooting.md diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..e5a5d8dd53 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,2 @@ +app: + image: gitlab/gitlab-ce:latest diff --git a/docker/.dockerignore b/docker/.dockerignore deleted file mode 100644 index dd449725e1..0000000000 --- a/docker/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -*.md diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 304bb97409..0000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER Sytse Sijbrandij - -# Install required packages -RUN apt-get update -q \ - && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ - ca-certificates \ - openssh-server \ - wget \ - apt-transport-https \ - vim \ - nano - -# Download & Install GitLab -# If you run GitLab Enterprise Edition point it to a location where you have downloaded it. -RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list -RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - -RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce - -# Manage SSHD through runit -RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ - && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ - && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ - && chmod a+x /opt/gitlab/sv/sshd/run \ - && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ - && mkdir -p /var/run/sshd - -# Disabling use DNS in ssh since it tends to slow connecting -RUN echo "UseDNS no" >> /etc/ssh/sshd_config - -# Prepare default configuration -RUN ( \ - echo "" && \ - echo "# Docker options" && \ - echo "# Prevent Postgres from trying to allocate 25% of total memory" && \ - echo "postgresql['shared_buffers'] = '1MB'" ) >> /etc/gitlab/gitlab.rb && \ - mkdir -p /assets/ && \ - cp /etc/gitlab/gitlab.rb /assets/gitlab.rb - -# Expose web & ssh -EXPOSE 443 80 22 - -# Define data volumes -VOLUME ["/etc/gitlab", "/var/opt/gitlab", "/var/log/gitlab"] - -# Copy assets -COPY assets/wrapper /usr/local/bin/ - -# Wrapper to handle signal, trigger runit and reconfigure GitLab -CMD ["/usr/local/bin/wrapper"] diff --git a/docker/README.md b/docker/README.md index e4d56cdb33..3423555a38 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,169 +1,7 @@ # GitLab Docker images -The GitLab docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). - -## After starting a container - -After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker. - -It might take a while before the docker container is responding to queries. - -You can check the status with something like `sudo docker logs -f gitlab`. - -You can login to the web interface with username `root` and password `5iveL!fe`. - -Next time, you can just use docker start and stop to run the container. - -## Run the image - -Run the image: -```bash -sudo docker run --detach \ - --publish 8443:443 --publish 8080:80 --publish 2222:22 \ - --name gitlab \ - --restart always \ - --volume /srv/gitlab/config:/etc/gitlab \ - --volume /srv/gitlab/logs:/var/log/gitlab \ - --volume /srv/gitlab/data:/var/opt/gitlab \ - gitlab/gitlab-ce:latest -``` - -This will download and start GitLab CE container and publish ports needed to access SSH, HTTP and HTTPS. -All GitLab data will be stored as subdirectories of `/srv/gitlab/`. -The container will automatically `restart` after system reboot. - -After this you can login to the web interface as explained above in 'After starting a container'. - -## Where is the data stored? - -The GitLab container uses host mounted volumes to store persistent data: -- `/srv/gitlab/data` mounted as `/var/opt/gitlab` in the container is used for storing *application data* -- `/srv/gitlab/logs` mounted as `/var/log/gitlab` in the container is used for storing *logs* -- `/srv/gitlab/config` mounted as `/etc/gitlab` in the container is used for storing *configuration* - -You can fine tune these directories to meet your requirements. - -### Configure GitLab - -This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. - -To access GitLab configuration, you can start an bash in a new the context of running container, you will be able to browse all directories and use your favorite text editor: -```bash -sudo docker exec -it gitlab /bin/bash -``` - -You can also edit just `/etc/gitlab/gitlab.rb`: -```bash -sudo docker exec -it gitlab vi /etc/gitlab/gitlab.rb -``` - -**You should set the `external_url` to point to a valid URL.** - -**You may also be interesting in [Enabling HTTPS](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#enable-https).** - -**To receive e-mails from GitLab you have to configure the [SMTP settings](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/smtp.md), -because Docker image doesn't have a SMTP server.** - -**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab: - -```bash -sudo docker restart gitlab -``` - -For more options for configuring the container please check [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). - -## Diagnose potential problems - -Read container logs: -```bash -sudo docker logs gitlab -``` - -Enter running container: -```bash -sudo docker exec -it gitlab /bin/bash -``` - -From within container you can administrer GitLab container as you would normally administer Omnibus installation: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md. - -### Upgrade GitLab to newer version - -To upgrade GitLab to new version you have to do: -1. pull new image, -```bash -sudo docker stop gitlab -``` - -1. stop running container, -```bash -sudo docker rm gitlab -``` - -1. remove existing container, -```bash -sudo docker pull gitlab/gitlab-ce:latest -``` - -1. create the container once again with previously specified options. -```bash -sudo docker run --detach \ - --publish 8443:443 --publish 8080:80 --publish 2222:22 \ - --name gitlab \ - --restart always \ - --volume /srv/gitlab/config:/etc/gitlab \ - --volume /srv/gitlab/logs:/var/log/gitlab \ - --volume /srv/gitlab/data:/var/opt/gitlab \ - gitlab/gitlab-ce:latest -``` - -On the first run GitLab will reconfigure and update itself. - -### Run GitLab CE on public IP address - -You can make Docker to use your IP address and forward all traffic to the GitLab CE container. -You can do that by modifying the `--publish` ([Binding container ports to the host](https://docs.docker.com/articles/networking/#binding-ports)): - -> --publish=[] : Publish a container᾿s port or a range of ports to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort - -To expose GitLab CE on IP 1.1.1.1: - -```bash -sudo docker run --detach \ - --publish 1.1.1.1:443:443 --publish 1.1.1.1:80:80 --publish 1.1.1.1:22:22 \ - --name gitlab \ - --restart always \ - --volume /srv/gitlab/config:/etc/gitlab \ - --volume /srv/gitlab/logs:/var/log/gitlab \ - --volume /srv/gitlab/data:/var/opt/gitlab \ - gitlab/gitlab-ce:latest -``` - -You can then access GitLab instance at http://1.1.1.1/ and https://1.1.1.1/. - -### Build the image - -This guide will also let you know how to build docker image yourself. -Please run the command from the GitLab repo root directory. -People using boot2docker should run all the commands without sudo. - -```bash -sudo docker build --tag gitlab/gitlab-ce:latest docker/ -``` - -### Publish the image to Dockerhub - -- Ensure the containers are running -- Login to Dockerhub with `sudo docker login` - -```bash -sudo docker login -sudo docker push gitlab/gitlab-ce:latest -``` - -## Troubleshooting - -Please see the [troubleshooting](troubleshooting.md) file in this directory. - -Note: We use `fig.yml` to have compatibility with fig and because docker-compose also supports it. - -Our docker image runs chef at every start to generate GitLab configuration. +* The official GitLab Community Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). +* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). +* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/) +* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker) +* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#Build-Docker-image) diff --git a/docker/assets/wrapper b/docker/assets/wrapper deleted file mode 100755 index 8bc8370fbc..0000000000 --- a/docker/assets/wrapper +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -function sigterm_handler() { - echo "SIGTERM signal received, try to gracefully shutdown all services..." - gitlab-ctl stop -} - -trap "sigterm_handler; exit" TERM - -function entrypoint() { - /opt/gitlab/embedded/bin/runsvdir-start & - gitlab-ctl reconfigure # will also start everything - gitlab-ctl tail # tail all logs -} - -if [[ ! -e /etc/gitlab/gitlab.rb ]]; then - cp /assets/gitlab.rb /etc/gitlab/gitlab.rb - chmod 0600 /etc/gitlab/gitlab.rb -fi - -entrypoint diff --git a/docker/fig.yml b/docker/fig.yml deleted file mode 100644 index 989551cbfe..0000000000 --- a/docker/fig.yml +++ /dev/null @@ -1,2 +0,0 @@ -app: - build: . diff --git a/docker/marathon.json b/docker/marathon.json deleted file mode 100644 index 9b2091a8c2..0000000000 --- a/docker/marathon.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "id": "/gitlab", - "ports": [0,0], - "cpus": 2, - "mem": 2048.0, - "disk": 10240.0, - "container": { - "type": "DOCKER", - "docker": { - "network": "HOST", - "image": "gitlab/gitlab-ce:latest" - }, - "volumes": [ - { - "containerPath": "/etc/gitlab", - "hostPath": "/var/data/etc/gitlab", - "mode": "RW" - }, - { - "containerPath": "/var/opt/gitlab", - "hostPath": "/var/data/opt/gitlab", - "mode": "RW" - }, - { - "containerPath": "/var/log/gitlab", - "hostPath": "/var/data/log/gitlab", - "mode": "RW" - } - ] - } -} \ No newline at end of file diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md deleted file mode 100644 index 63482547da..0000000000 --- a/docker/troubleshooting.md +++ /dev/null @@ -1,84 +0,0 @@ -# Troubleshooting - -This is to troubleshoot https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/245 -But it might contain useful commands for other cases as well. - -The configuration to add the postgres log in vim is: -postgresql['log_directory'] = '/var/log/gitlab/postgresql' - -# Commands - -```bash -sudo docker build --tag gitlab/gitlab-ce:latest docker/ - -sudo docker rm -f gitlab - -sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb - -sudo docker exec gitlab tail -f /var/log/gitlab/reconfigure.log - -sudo docker exec gitlab tail -f /var/log/gitlab/postgresql/current - -sudo docker exec gitlab cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers - -sudo docker exec gitlab cat /etc/gitlab/gitlab.rb -``` - -# Interactively - -```bash -# First start a GitLab container without starting GitLab -# This is almost the same as starting the GitLab container except: -# - we run interactively (-t -i) -# - we define TERM=linux because it allows to use arrow keys in vi (!!!) -# - we choose another startup command (bash) -sudo docker run --ti \ - -e TERM=linux - --publish 80443:443 --publish 8080:80 --publish 2222:22 \ - --name gitlab \ - --restart always \ - --volume /srv/gitlab/config:/etc/gitlab \ - --volume /srv/gitlab/logs:/var/log/gitlab \ - --volume /srv/gitlab/data:/var/opt/gitlab \ - gitlab/gitlab-ce:latest \ - bash - -# Configure GitLab to redirect PostgreSQL logs -echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb - -# Prevent Postgres from allocating 25% of total memory -echo "postgresql['shared_buffers'] = '1MB'" >> /etc/gitlab/gitlab.rb - -# You can now start GitLab manually from Bash (in the background) -# Maybe the command below is still missing something to run in the background -gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start & - -# Inspect PostgreSQL config -cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers - -# And tail the logs (PostgreSQL log may not exist immediately) -tail -f /var/log/gitlab/reconfigure.log /var/log/gitlab/postgresql/current - -# And get the memory -cat /proc/meminfo -head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall -free -m - -``` - -# Cleanup - -Remove ALL docker containers and images (also non GitLab ones). -**Be careful, because the `-v` also removes volumes attached to the images.** - -```bash -# Remove all containers with attached volumes -docker rm -v $(docker ps -a -q) - -# Remove all images -docker rmi $(docker images -q) - -# Remove GitLab persistent data -rm -rf /srv/gitlab -``` - From 46503b789def6a0bb82a7bf559e8488a056e3f1f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 30 Sep 2015 15:29:52 +0200 Subject: [PATCH 090/543] Fix invalid tests Signed-off-by: Dmitriy Zaporozhets --- features/project/merge_requests.feature | 36 ++++++++++++------------ features/steps/project/merge_requests.rb | 36 ++++++++++++------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 947f668e43..83055188ba 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -115,40 +115,40 @@ Feature: Project Merge Requests Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - Then I should not see a comment like "Line is wrong here" in the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + Then I should not see a comment like "Line is wrong here" in the third file @javascript Scenario: I show comments on a merge request diff with comments in a single file Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is wrong" on line 39 of the second file - Then I should see a comment like "Line is wrong" in the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + Then I should see a comment like "Line is wrong" in the third file @javascript Scenario: I hide comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - Then I should not see a comment like "Line is wrong here" in the second file - And I should still see a comment like "Line is correct" in the first file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + Then I should not see a comment like "Line is wrong here" in the third file + And I should still see a comment like "Line is correct" in the second file @javascript Scenario: I show comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - And I click link "Show inline discussion" of the second file - Then I should see a comment like "Line is wrong" in the second file - And I should still see a comment like "Line is correct" in the first file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + And I click link "Show inline discussion" of the third file + Then I should see a comment like "Line is wrong" in the third file + And I should still see a comment like "Line is correct" in the second file @javascript Scenario: I unfold diff @@ -163,8 +163,8 @@ Feature: Project Merge Requests Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file And I click Side-by-side Diff tab Then I should see comments on the side-by-side diff page diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index c92998631f..875bf6c467 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -224,43 +224,43 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I click link "Hide inline discussion" of the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I click link "Hide inline discussion" of the third file' do + page.within '.files [id^=diff]:nth-child(3)' do find('.js-toggle-diff-comments').trigger('click') end end - step 'I click link "Show inline discussion" of the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I click link "Show inline discussion" of the third file' do + page.within '.files [id^=diff]:nth-child(3)' do find('.js-toggle-diff-comments').trigger('click') end end - step 'I should not see a comment like "Line is wrong" in the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I should not see a comment like "Line is wrong" in the third file' do + page.within '.files [id^=diff]:nth-child(3)' do expect(page).not_to have_visible_content "Line is wrong" end end - step 'I should see a comment like "Line is wrong" in the second file' do - page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + step 'I should see a comment like "Line is wrong" in the third file' do + page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do expect(page).to have_visible_content "Line is wrong" end end - step 'I should not see a comment like "Line is wrong here" in the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I should not see a comment like "Line is wrong here" in the third file' do + page.within '.files [id^=diff]:nth-child(3)' do expect(page).not_to have_visible_content "Line is wrong here" end end - step 'I should see a comment like "Line is wrong here" in the second file' do - page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + step 'I should see a comment like "Line is wrong here" in the third file' do + page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do expect(page).to have_visible_content "Line is wrong here" end end - step 'I leave a comment like "Line is correct" on line 12 of the first file' do + step 'I leave a comment like "Line is correct" on line 12 of the second file' do init_diff_note_first_file page.within(".js-discussion-note-form") do @@ -268,12 +268,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Add Comment" end - page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do + page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do expect(page).to have_content "Line is correct" end end - step 'I leave a comment like "Line is wrong" on line 39 of the second file' do + step 'I leave a comment like "Line is wrong" on line 39 of the third file' do init_diff_note_second_file page.within(".js-discussion-note-form") do @@ -282,8 +282,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I should still see a comment like "Line is correct" in the first file' do - page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do + step 'I should still see a comment like "Line is correct" in the second file' do + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do expect(page).to have_visible_content "Line is correct" end end @@ -303,7 +303,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see comments on the side-by-side diff page' do - page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do + page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do expect(page).to have_visible_content "Line is correct" end end From f4eaae57679287b3e92a943b50a66ddb641350d9 Mon Sep 17 00:00:00 2001 From: Dimitar Dimitrov Date: Wed, 30 Sep 2015 16:30:13 +0300 Subject: [PATCH 091/543] Add a missing RAILS_ENV=production to crontab clearing All other commands in the guide set this env var, but it's missing here and this causes whenever not to clear the crontab file of gitab_ci properly. --- doc/migrate_ci_to_ce/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 2725bf343e..56bf7a1418 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -143,7 +143,7 @@ sudo gitlab-ctl stop ci-sidekiq # Source sudo service gitlab_ci stop cd /home/gitlab_ci/gitlab-ci -sudo -u gitlab_ci -H bundle exec whenever --clear-crontab +sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production ``` ### II. Moving data From 63a6259328b602b0392f3b2eb1cd2adf4d275bc5 Mon Sep 17 00:00:00 2001 From: Diogo Resende Date: Wed, 30 Sep 2015 17:26:55 +0000 Subject: [PATCH 092/543] Adds missing plural form of applications of previous commit --- app/views/admin/applications/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index 6ac45a3db1..db8d61ec6d 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -2,7 +2,7 @@ %h3.page-title System OAuth applications %p.light - System OAuth application don't belong to any user and can only be managed by admins + System OAuth applications don't belong to any user and can only be managed by admins %hr %p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success' %table.table.table-striped From bbb845b20b7b6cced50e28d532d13ec979a0db66 Mon Sep 17 00:00:00 2001 From: Diogo Resende Date: Wed, 30 Sep 2015 17:28:06 +0000 Subject: [PATCH 093/543] Adds newline at end of file --- app/views/admin/applications/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index db8d61ec6d..f8cd98f0ec 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -20,4 +20,4 @@ %td= application.redirect_uri %td= application.access_tokens.map(&:resource_owner_id).uniq.count %td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link' - %td= render 'delete_form', application: application \ No newline at end of file + %td= render 'delete_form', application: application From 5a401523ebb756faf28b74301fae3901ef929537 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 30 Sep 2015 19:43:29 +0200 Subject: [PATCH 094/543] Fix migration --- db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb index a978fcda3b..8d47dac644 100644 --- a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb +++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb @@ -1,4 +1,4 @@ -class ChangeNameOfCiProjects < ActiveRecord::Migration +class AddNullToNameForCiProjects < ActiveRecord::Migration def up change_column_null :ci_projects, :name, true end From f310c84b1d595b2eb06aa76d85d127bfd43158b5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 30 Sep 2015 19:54:56 +0200 Subject: [PATCH 095/543] Fix typo --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 3423555a38..7514d610ae 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,7 +1,7 @@ # GitLab Docker images * The official GitLab Community Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). -* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). +* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ee/). * The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/) * The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker) * Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#Build-Docker-image) From 3a4274e19e1a1fbc23fb5fe0d6101ad62099aadb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 14:35:00 -0400 Subject: [PATCH 096/543] Take advantage of `Devise.sign_in_after_reset_password` --- app/controllers/passwords_controller.rb | 21 --------- config/initializers/devise.rb | 4 ++ spec/features/password_reset_spec.rb | 58 ++++++++++++------------- 3 files changed, 33 insertions(+), 50 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 8450ba3102..edf43935f3 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -16,27 +16,6 @@ class PasswordsController < Devise::PasswordsController end end - # After a user resets their password, prompt for 2FA code if enabled instead - # of signing in automatically - # - # See http://git.io/vURrI - def update - super do |resource| - # TODO (rspeicher): In Devise master (> 3.4.1), we can set - # `Devise.sign_in_after_reset_password = false` and avoid this mess. - if resource.errors.empty? && resource.try(:two_factor_enabled?) - resource.unlock_access! if unlockable?(resource) - - # Since we are not signing this user in, we use the :updated_not_active - # message which only contains "Your password was changed successfully." - set_flash_message(:notice, :updated_not_active) if is_flashing_format? - - # Redirect to sign in so they can enter 2FA code - respond_with(resource, location: new_session_path(resource)) and return - end - end - end - def edit super reset_password_token = Devise.token_generator.digest( diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 2ce24592f8..29506970af 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -148,6 +148,10 @@ Devise.setup do |config| # When someone else invites you to GitLab this time is also used so it should be pretty long. config.reset_password_within = 2.days + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + config.sign_in_after_reset_password = false + # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 2b6311e4fd..abf66f2356 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -1,6 +1,35 @@ require 'spec_helper' feature 'Password reset', feature: true do + describe 'with two-factor authentication' do + let(:user) { create(:user, :two_factor) } + + it 'requires login after password reset' do + visit root_path + + forgot_password + reset_password + + expect(page).to have_content("Your password was changed successfully.") + expect(page).not_to have_content("You are now signed in.") + expect(current_path).to eq new_user_session_path + end + end + + describe 'without two-factor authentication' do + let(:user) { create(:user) } + + it 'requires login after password reset' do + visit root_path + + forgot_password + reset_password + + expect(page).to have_content("Your password was changed successfully.") + expect(current_path).to eq new_user_session_path + end + end + def forgot_password click_on 'Forgot your password?' fill_in 'Email', with: user.email @@ -21,33 +50,4 @@ feature 'Password reset', feature: true do fill_in 'Confirm new password', with: password click_button 'Change your password' end - - describe 'with two-factor authentication' do - let(:user) { create(:user, :two_factor) } - - it 'requires login after password reset' do - visit root_path - - forgot_password - reset_password - - expect(page).to have_content("Your password was changed successfully.") - expect(page).not_to have_content("You are now signed in.") - expect(current_path).to eq new_user_session_path - end - end - - describe 'without two-factor authentication' do - let(:user) { create(:user) } - - it 'automatically logs in after password reset' do - visit root_path - - forgot_password - reset_password - - expect(current_path).to eq root_path - expect(page).to have_content("Your password was changed successfully. You are now signed in.") - end - end end From 292bca0546c59b9816c696371cd9bbf04ba19fb2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 15:38:21 -0400 Subject: [PATCH 097/543] Only allow password reset emails once per minute Addresses internal https://dev.gitlab.org/gitlab/gitlabhq/issues/2611 --- app/controllers/passwords_controller.rb | 22 +++++++++---- spec/features/password_reset_spec.rb | 43 +++++++++++++++++++++---- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index edf43935f3..a2d152addc 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -2,18 +2,19 @@ class PasswordsController < Devise::PasswordsController def create email = resource_params[:email] - resource_found = resource_class.find_by_email(email) - if resource_found && resource_found.ldap_user? + self.resource = resource_class.find_by_email(email) + + if resource && resource.ldap_user? flash[:alert] = "Cannot reset password for LDAP user." respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return end - self.resource = resource_class.send_reset_password_instructions(resource_params) - if successfully_sent?(resource) - respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) - else - respond_with(resource) + unless can_send_reset_email? + flash[:alert] = "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again." + respond_with({}, location: new_password_path(resource_name)) and return end + + super end def edit @@ -35,4 +36,11 @@ class PasswordsController < Devise::PasswordsController end end end + + private + + def can_send_reset_email? + resource && (resource.reset_password_sent_at.blank? || + resource.reset_password_sent_at < 1.minute.ago) + end end diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index abf66f2356..ce7a66a0da 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -1,13 +1,44 @@ require 'spec_helper' feature 'Password reset', feature: true do - describe 'with two-factor authentication' do - let(:user) { create(:user, :two_factor) } + describe 'throttling' do + it 'sends reset instructions when not previously sent' do + visit root_path + forgot_password(create(:user)) + expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(current_path).to eq new_user_session_path + end + + it 'sends reset instructions when previously sent more than a minute ago' do + user = create(:user) + user.send_reset_password_instructions + user.update_attribute(:reset_password_sent_at, 5.minutes.ago) + + visit root_path + forgot_password(user) + + expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(current_path).to eq new_user_session_path + end + + it "throttles multiple resets in a short timespan" do + user = create(:user) + user.send_reset_password_instructions + + visit root_path + forgot_password(user) + + expect(page).to have_content("Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again.") + expect(current_path).to eq new_user_password_path + end + end + + describe 'with two-factor authentication' do it 'requires login after password reset' do visit root_path - forgot_password + forgot_password(create(:user, :two_factor)) reset_password expect(page).to have_content("Your password was changed successfully.") @@ -17,12 +48,10 @@ feature 'Password reset', feature: true do end describe 'without two-factor authentication' do - let(:user) { create(:user) } - it 'requires login after password reset' do visit root_path - forgot_password + forgot_password(create(:user)) reset_password expect(page).to have_content("Your password was changed successfully.") @@ -30,7 +59,7 @@ feature 'Password reset', feature: true do end end - def forgot_password + def forgot_password(user) click_on 'Forgot your password?' fill_in 'Email', with: user.email click_button 'Reset password' From 5c80ceee0d3545061f1ca85ad8c25d0583e1e4d2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 15:39:59 -0400 Subject: [PATCH 098/543] Autofocus the email field on the password reset form --- app/views/devise/passwords/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 29ffe8a8be..535e85869e 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -6,7 +6,7 @@ .devise-errors = devise_error_messages! .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email] + = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true .clearfix = f.submit "Reset password", class: "btn-primary btn" From ceb21cc49f27ddfade4d28e7ad8805d481706922 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 15:47:48 -0400 Subject: [PATCH 099/543] Update feature spec --- spec/features/password_reset_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index ce7a66a0da..deb90a4450 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -55,6 +55,7 @@ feature 'Password reset', feature: true do reset_password expect(page).to have_content("Your password was changed successfully.") + expect(page).not_to have_content("You are now signed in.") expect(current_path).to eq new_user_session_path end end From 9052f13b31944cc1c69af3dec8176fde0bb080a6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Sep 2015 16:15:56 -0400 Subject: [PATCH 100/543] Remove specs for "login after reset" We're now using default Devise behavior, so these tests were redundant. --- spec/features/password_reset_spec.rb | 40 ---------------------------- 1 file changed, 40 deletions(-) diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index deb90a4450..4d512c6543 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -34,50 +34,10 @@ feature 'Password reset', feature: true do end end - describe 'with two-factor authentication' do - it 'requires login after password reset' do - visit root_path - - forgot_password(create(:user, :two_factor)) - reset_password - - expect(page).to have_content("Your password was changed successfully.") - expect(page).not_to have_content("You are now signed in.") - expect(current_path).to eq new_user_session_path - end - end - - describe 'without two-factor authentication' do - it 'requires login after password reset' do - visit root_path - - forgot_password(create(:user)) - reset_password - - expect(page).to have_content("Your password was changed successfully.") - expect(page).not_to have_content("You are now signed in.") - expect(current_path).to eq new_user_session_path - end - end - def forgot_password(user) click_on 'Forgot your password?' fill_in 'Email', with: user.email click_button 'Reset password' user.reload end - - def get_reset_token - mail = ActionMailer::Base.deliveries.last - body = mail.body.encoded - body.scan(/reset_password_token=(.+)\"/).flatten.first - end - - def reset_password(password = 'password') - visit edit_user_password_path(reset_password_token: get_reset_token) - - fill_in 'New password', with: password - fill_in 'Confirm new password', with: password - click_button 'Change your password' - end end From 9ade9a64519395d77b6ef143ad063103d6ff3d10 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 25 Sep 2015 16:27:41 -0400 Subject: [PATCH 101/543] Don't render the edit_form view for system notes --- app/views/projects/notes/_note.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 9bfbde02ca..cf5d5d6d8b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -59,7 +59,9 @@ .note-text = preserve do = markdown(note.note, {no_header_anchors: true}) - = render 'projects/notes/edit_form', note: note + - unless note.system? + -# System notes can't be edited + = render 'projects/notes/edit_form', note: note - if note.attachment.url .note-attachment From 7c20f30f05d06fe2571bceed2b1aa3c5d38e9daa Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 25 Sep 2015 16:27:58 -0400 Subject: [PATCH 102/543] Ensure notes are highlighted properly when they're updated --- app/assets/javascripts/notes.js.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ce638c2641..4b9f0d6891 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -277,13 +277,15 @@ class @Notes Updates the current note field. ### - updateNote: (xhr, note, status) => - note_li = $(".note-row-" + note.id) - note_li.replaceWith(note.html) - note_li.find('.note-edit-form').hide() - note_li.find('.note-body > .note-text').show() - note_li.find('js-task-list-container').taskList('enable') - @enableTaskList() + updateNote: (_xhr, note, _status) => + # Convert returned HTML to a jQuery object so we can modify it further + $html = $(note.html) + $html.syntaxHighlight() + $html.find('.js-task-list-container').taskList('enable') + + # Find the note's `li` element by ID and replace it with the updated HTML + $note_li = $("#note_#{note.id}") + $note_li.replaceWith($html) ### Called in response to clicking the edit note link From da347f360273805b782602e38785c7dbf012edf2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 27 Sep 2015 16:14:09 -0400 Subject: [PATCH 103/543] Simplify Note#editable? --- app/models/note.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/note.rb b/app/models/note.rb index 89d81ab1de..de3b6df88f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -366,6 +366,6 @@ class Note < ActiveRecord::Base end def editable? - !read_attribute(:system) + !system? end end From e97349cb42e5886f6e9e960851e38b4f0c2036e5 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Wed, 12 Aug 2015 17:31:59 -0400 Subject: [PATCH 104/543] notify: fix typo in merged_merge_request_email template All (two) other templates with this line use 'url' rather than 'Url'. --- app/views/notify/merged_merge_request_email.text.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 9db75bdb19..34dbc60e19 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was merged" -Merge Request Url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') From 5544d54793c49b072e7b46b847d01307ce3754e7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 30 Sep 2015 20:50:53 -0700 Subject: [PATCH 105/543] Include full path of source and target branch names in New Merge Request page Previous title would only show the group name, which could be confusing. Closes #2875 --- CHANGELOG | 1 + app/helpers/merge_requests_helper.rb | 13 ++++++++ .../merge_requests/_new_submit.html.haml | 5 +-- spec/helpers/merge_requests_helper.rb | 12 ------- spec/helpers/merge_requests_helper_spec.rb | 32 +++++++++++++++++++ 5 files changed, 49 insertions(+), 14 deletions(-) delete mode 100644 spec/helpers/merge_requests_helper.rb create mode 100644 spec/helpers/merge_requests_helper_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 492e4b9aeb..9f5983c049 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) + - 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) - Show CI status on all pages where commits list is rendered diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index f8169b4f28..81773e7afc 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -71,4 +71,17 @@ module MergeRequestsHelper merge_request.source_branch end end + + def format_mr_branch_names(merge_request) + source_path = merge_request.source_project_path + target_path = merge_request.target_project_path + source_branch = merge_request.source_branch + target_branch = merge_request.target_branch + + if source_path == target_path + [source_branch, target_branch] + else + ["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"] + end + end end diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 46aeecd873..6244d3ba0b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -1,10 +1,11 @@ %h3.page-title New merge request %p.slead + - source_title, target_title = format_mr_branch_names(@merge_request) From - %strong.label-branch #{@merge_request.source_project_namespace}:#{@merge_request.source_branch} + %strong.label-branch #{source_title} %span into - %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} + %strong.label-branch #{target_title} %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb deleted file mode 100644 index 5262d64404..0000000000 --- a/spec/helpers/merge_requests_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe MergeRequestsHelper do - describe :issues_sentence do - subject { issues_sentence(issues) } - let(:issues) do - [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] - end - - it { is_expected.to eq('#1, #2, and #3') } - end -end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb new file mode 100644 index 0000000000..0ef1efb8bc --- /dev/null +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe MergeRequestsHelper do + describe "#issues_sentence" do + subject { issues_sentence(issues) } + let(:issues) do + [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] + end + + it { is_expected.to eq('#1, #2, and #3') } + end + + describe "#format_mr_branch_names" do + describe "within the same project" do + let(:merge_request) { create(:merge_request) } + subject { format_mr_branch_names(merge_request) } + + it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) } + end + + describe "within different projects" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + subject { format_mr_branch_names(merge_request) } + let(:source_title) { "#{fork_project.path_with_namespace}:#{merge_request.source_branch}" } + let(:target_title) { "#{project.path_with_namespace}:#{merge_request.target_branch}" } + + it { is_expected.to eq([source_title, target_title]) } + end + end +end From 05fdd12fd984ffee0b2c9be3821fbc9a67abc6d4 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 1 Oct 2015 09:31:48 +0300 Subject: [PATCH 106/543] Improve error message when merging fails --- CHANGELOG | 1 + .../javascripts/merge_request_widget.js.coffee | 11 ++++++----- .../projects/merge_requests_controller.rb | 1 + app/services/merge_requests/merge_service.rb | 4 ++++ .../20150930001110_merge_request_error_field.rb | 5 +++++ db/schema.rb | 3 ++- spec/services/merge_requests/merge_service_spec.rb | 14 ++++++++++++++ 7 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20150930001110_merge_request_error_field.rb diff --git a/CHANGELOG b/CHANGELOG index 492e4b9aeb..dc8cac6769 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,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 + - Improve error message when merging fails v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 995a2f2409..3176e5a896 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -15,11 +15,12 @@ class @MergeRequestWidget type: 'GET' url: $('.merge-request').data('url') success: (data) => - switch data.state - when 'merged' - location.reload() - else - setTimeout(merge_request_widget.mergeInProgress, 2000) + if data.state == "merged" + location.reload() + else if data.merge_error + $('.mr-widget-body').html("

" + data.merge_error + "

") + else + setTimeout(merge_request_widget.mergeInProgress, 2000) dataType: 'json' getMergeStatus: -> diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7574842cd4..7570934e72 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -150,6 +150,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return access_denied! unless @merge_request.can_be_merged_by?(current_user) if @merge_request.mergeable? + @merge_request.update(merge_error: nil) MergeWorker.perform_async(@merge_request.id, current_user.id, params) @status = true else diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 98a67c0bc9..fcc0f2a6a8 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -38,6 +38,10 @@ module MergeRequests } repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) + rescue Exception => e + merge_request.update(merge_error: "Something went wrong during merge") + Rails.logger.error(e.message) + return false end def after_merge diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb new file mode 100644 index 0000000000..c2ee498ef3 --- /dev/null +++ b/db/migrate/20150930001110_merge_request_error_field.rb @@ -0,0 +1,5 @@ +class MergeRequestErrorField < ActiveRecord::Migration + def up + add_column :merge_requests, :merge_error, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 4ce6cee86e..0aac301587 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: 20150930001110) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -453,6 +453,7 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" + t.string "merge_error" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 7b564d34d7..7483f51de0 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -35,5 +35,19 @@ describe MergeRequests::MergeService do expect(note.note).to include 'Status changed to merged' end end + + context "error handling" do + let(:service) { MergeRequests::MergeService.new(project, user, {}) } + + it 'saves error if there is an exception' do + allow(service).to receive(:repository).and_raise("error") + + allow(service).to receive(:execute_hooks) + + service.execute(merge_request, 'Awesome message') + + expect(merge_request.merge_error).to eq("Something went wrong during merge") + end + end end end From 5b573130515913dd29216c642334200b1b973245 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 29 Sep 2015 16:37:50 +0300 Subject: [PATCH 107/543] Note the original location of a moved project when notifying users of the move --- CHANGELOG | 1 + app/mailers/emails/projects.rb | 3 ++- app/models/namespace.rb | 4 +++- app/models/project.rb | 6 +++--- app/services/notification_service.rb | 4 ++-- app/services/projects/transfer_service.rb | 2 +- app/views/notify/project_was_moved_email.html.haml | 2 +- app/views/notify/project_was_moved_email.text.erb | 2 +- spec/mailers/notify_spec.rb | 2 +- spec/services/notification_service_spec.rb | 6 +++--- 10 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 492e4b9aeb..ef4ada7e1e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,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 + - Note the original location of a moved project when notifying users of the move v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 4a6e18e6a7..caba63006d 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -50,10 +50,11 @@ module Emails subject: subject("Invitation declined")) end - def project_was_moved_email(project_id, user_id) + def project_was_moved_email(project_id, user_id, old_path_with_namespace) @current_user = @user = User.find user_id @project = Project.find project_id @target_url = namespace_project_url(@project.namespace, @project) + @old_path_with_namespace = old_path_with_namespace mail(to: @user.notification_email, subject: subject("Project was moved")) end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 161a16ca61..bc8525df5a 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -137,7 +137,9 @@ class Namespace < ActiveRecord::Base end def send_update_instructions - projects.each(&:send_move_instructions) + projects.each do |project| + project.send_move_instructions("#{path_was}/#{project.path}") + end end def kind diff --git a/app/models/project.rb b/app/models/project.rb index 953b37e3f7..f91c33e26a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -481,8 +481,8 @@ class Project < ActiveRecord::Base end end - def send_move_instructions - NotificationService.new.project_was_moved(self) + def send_move_instructions(old_path_with_namespace) + NotificationService.new.project_was_moved(self, old_path_with_namespace) end def owner @@ -624,7 +624,7 @@ class Project < ActiveRecord::Base # So we basically we mute exceptions in next actions begin gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") - send_move_instructions + send_move_instructions(old_path_with_namespace) reset_events_cache rescue # Returning false does not rollback after_* transaction but gives diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e294b23bc2..a6b2234865 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -183,12 +183,12 @@ class NotificationService mailer.group_access_granted_email(group_member.id) end - def project_was_moved(project) + def project_was_moved(project, old_path_with_namespace) recipients = project.team.members recipients = reject_muted_users(recipients, project) recipients.each do |recipient| - mailer.project_was_moved_email(project.id, recipient.id) + mailer.project_was_moved_email(project.id, recipient.id, old_path_with_namespace) end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 550ed6897d..c327c244f0 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -38,7 +38,7 @@ module Projects project.save! # Notifications - project.send_move_instructions + project.send_move_instructions(old_path) # Move main repository unless gitlab_shell.mv_repository(old_path, new_path) diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index 3cd759f1f5..87b3ff7f0b 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -1,5 +1,5 @@ %p - Project was moved to another location + Project #{@old_path_with_namespace} was moved to another location %p The project is now located under = link_to namespace_project_url(@project.namespace, @project) do diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb index b3f18b35a4..d8a23dabf4 100644 --- a/app/views/notify/project_was_moved_email.text.erb +++ b/app/views/notify/project_was_moved_email.text.erb @@ -1,4 +1,4 @@ -Project was moved to another location +Project #{@old_path_with_namespace} was moved to another location The project is now located under <%= namespace_project_url(@project.namespace, @project) %> diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 2c97a521d9..42481a9ea3 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -399,7 +399,7 @@ describe Notify do describe 'project was moved' do let(:project) { create(:project) } let(:user) { create(:user) } - subject { Notify.project_was_moved_email(project.id, user.id) } + subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } it_behaves_like 'an email sent from GitLab' diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 8865335d0d..520140917a 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -427,15 +427,15 @@ describe NotificationService do should_email(@u_watcher.id) should_email(@u_participating.id) should_not_email(@u_disabled.id) - notification.project_was_moved(project) + notification.project_was_moved(project, "gitlab/gitlab") end def should_email(user_id) - expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") end def should_not_email(user_id) - expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") end end end From 2e92600b30b4e935fb8ba125d99124539545d901 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 1 Oct 2015 01:44:50 -0700 Subject: [PATCH 108/543] Fix Message-ID field to be RFC 2111-compliant to prevent e-mails from being dropped Closes #2867 --- CHANGELOG | 1 + app/mailers/notify.rb | 2 +- spec/mailers/notify_spec.rb | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 492e4b9aeb..d6488ca1b9 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 Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (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) - Show CI status on all pages where commits list is rendered diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index db2f9654e1..50a409c375 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -140,7 +140,7 @@ class Notify < BaseMailer # * have a 'In-Reply-To' or 'References' header that references the original 'Message-ID' # def mail_answer_thread(model, headers = {}) - headers['Message-ID'] = SecureRandom.hex + headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>" headers['In-Reply-To'] = message_id(model) headers['References'] = message_id(model) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 2c97a521d9..f219af67ff 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -52,6 +52,7 @@ describe Notify do end it 'has headers that reference an existing thread' do + is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ From 6ca5a258cadfef86359756e5dc3ab9f0862e22b9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 11:33:46 +0200 Subject: [PATCH 109/543] Update db schema Signed-off-by: Dmitriy Zaporozhets --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 0aac301587..72609da93f 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: 20150930001110) do +ActiveRecord::Schema.define(version: 20150930095736) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From bb6a39892d64aa50917fc9c4f57904e2b047f5c6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 1 Oct 2015 11:34:29 +0200 Subject: [PATCH 110/543] Update migration documentation --- doc/migrate_ci_to_ce/README.md | 105 +++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 56bf7a1418..46ce0fe98c 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -36,6 +36,11 @@ mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s) and run `sudo gitlab-ctl reconfigure`. +#### 0. Updating Omnibus from versions prior to 7.13 + +If you are updating from older versions you should first update to 7.14 and then to 8.0. +Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting). + #### 1. Verify that backups work Make sure that the backup script on both servers can connect to the database. @@ -43,6 +48,7 @@ Make sure that the backup script on both servers can connect to the database. ``` # On your CI server: # Omnibus +sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds sudo gitlab-ci-rake backup:create # Source @@ -319,3 +325,102 @@ You should also make sure that you can: If something went wrong and you need to restore a backup, consult the [Backup restoration](../raketasks/backup_restore.md) guide. + +### Troubleshooting + +#### show:secrets problem (Omnibus-only) +If you see errors like this: +``` +Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml` +rake aborted! +Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml +``` + +This can happen if you are updating from versions prior to 7.13 straight to 8.0. +The fix for this is to update to Omnibus 7.14 first and then update it to 8.0. + +#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds +To fix that issue you have to change builds/ folder permission before doing final backup: +``` +chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds +``` + +#### Problems when importing CI database to GitLab +If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences: +``` +ALTER SEQUENCE +ERROR: relation "ci_builds_id_seq" does not exist +ERROR: relation "ci_commits_id_seq" does not exist +ERROR: relation "ci_events_id_seq" does not exist +ERROR: relation "ci_jobs_id_seq" does not exist +ERROR: relation "ci_projects_id_seq" does not exist +ERROR: relation "ci_runner_projects_id_seq" does not exist +ERROR: relation "ci_runners_id_seq" does not exist +ERROR: relation "ci_services_id_seq" does not exist +ERROR: relation "ci_taggings_id_seq" does not exist +ERROR: relation "ci_tags_id_seq" does not exist +CREATE TABLE +``` + +To fix that you need to apply this SQL statement before doing final backup: +``` +# Omnibus +gitlab-ci-rails dbconsole < Date: Thu, 1 Oct 2015 11:42:41 +0200 Subject: [PATCH 111/543] Fix: broken git clone URL for CI runners --- app/models/ci/project.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index f0a8fc703b..24f7017109 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -169,8 +169,7 @@ module Ci # using http and basic auth def repo_url_with_auth auth = "gitlab-ci-token:#{token}@" - url = http_url_to_repo + ".git" - url.sub(/^https?:\/\//) do |prefix| + http_url_to_repo.sub(/^https?:\/\//) do |prefix| prefix + auth end end From 0e548473397f96246f3f70548e48d41fc98ebf00 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 1 Oct 2015 11:43:06 +0200 Subject: [PATCH 112/543] Fix: CI token removal regression from build trace --- app/models/ci/build.rb | 17 ++++++++++------- spec/models/ci/build_spec.rb | 11 +++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cda4fdd498..19f957eb96 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -143,12 +143,6 @@ module Ci html ||= '' end - def trace - if project && read_attribute(:trace).present? - read_attribute(:trace).gsub(project.token, 'xxxxxx') - end - end - def started? !pending? && !canceled? && started_at end @@ -223,7 +217,7 @@ module Ci end end - def trace + def raw_trace if File.exist?(path_to_trace) File.read(path_to_trace) else @@ -232,6 +226,15 @@ module Ci end end + def trace + trace = raw_trace + if project && trace.present? + trace.gsub(project.token, 'xxxxxx') + else + trace + end + end + def trace=(trace) unless Dir.exists? dir_to_trace FileUtils.mkdir_p dir_to_trace diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 82623bd819..ca070a1497 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -178,6 +178,17 @@ describe Ci::Build do it { is_expected.to include(text) } it { expect(subject.length).to be >= text.length } end + + context 'if build.trace hides token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update_attributes(token: token) + build.update_attributes(trace: token) + end + + it { is_expected.to_not include(token) } + end end describe :timeout do From 6718f800ce16af4b402f4bff40d1850e708b9067 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 12:19:08 +0200 Subject: [PATCH 113/543] Fix UI bug introduced in new project page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 82 +++++++++++----------- app/views/projects/show.html.haml | 2 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3105178567..818aa10aef 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 { +.project-footer { 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; diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 6a5fc68980..efa119edd5 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -75,7 +75,7 @@ - if current_user - access = user_max_access_in_project(current_user, @project) - if access - .prepend-top-20 + .prepend-top-20.project-footer .gray-content-block.footer-block.center You have #{access} access to this project. - if @project.project_member_by_id(current_user) From b7629a53272bbd4bcd2fe4f98a6c2a7e28534089 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 29 Sep 2015 14:54:13 +0200 Subject: [PATCH 114/543] Update gitlab_git to 7.2.17. --- Gemfile | 2 +- Gemfile.lock | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 6950091b2b..f1674c560e 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.15' +gem "gitlab_git", '~> 7.2.17' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 4386c6b9ab..6472221bc5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.1) gemojione (~> 2.0) - gitlab_git (7.2.15) + gitlab_git (7.2.17) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -834,7 +834,7 @@ DEPENDENCIES gitlab-flowdock-git-hook (~> 1.0.1) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.15) + gitlab_git (~> 7.2.17) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) @@ -937,3 +937,6 @@ DEPENDENCIES webmock (~> 1.21.0) whenever (~> 0.8.4) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.10.6 From edd4ff28a96038430a9b3c0d2d560ece1dfda79a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 12:44:55 +0200 Subject: [PATCH 115/543] Improve UI for list of changed files Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/commit.scss | 27 +++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 051ca3792c..e14203f827 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -49,30 +49,33 @@ } .file-stats { + ul { + list-style: none; + margin: 0; + padding: 10px 0; + + li { + padding: 3px 0px; + } + } .new-file { a { - color: #090; - } - i { - color: #1BCF00; + color: $gl-success; } } .renamed-file { - i { - color: #FE9300; + a { + color: $gl-warning; } } .deleted-file { a { - color: #B00; - } - i { - color: #EE0000; + color: $gl-danger; } } .edit-file{ - i{ - color: #555; + a { + color: $gl-text-color; } } } From 3871498b32a706ae27894f4a96ab326382e22afd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 12:55:11 +0200 Subject: [PATCH 116/543] Better colors for text Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/base/variables.scss | 5 ++++- app/assets/stylesheets/generic/common.scss | 4 ++-- app/assets/stylesheets/pages/commit.scss | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index f6bdea9a89..befd63832d 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,5 +1,8 @@ $hover: #FFFAF1; -$gl-text-color: #54565b; +$gl-text-color: #54565B; +$gl-text-green: #4A2; +$gl-text-red: #D12F19; +$gl-text-orange: #D90; $gl-header-color: #4c4e54; $gl-link-color: #333c48; $md-text-color: #444; diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 96fb791c24..016cc015e9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -1,8 +1,8 @@ /** COLORS **/ .cgray { color: $gl-gray; } .clgray { color: #BBB } -.cred { color: #D12F19 } -.cgreen { color: #4a2 } +.cred { color: $gl-text-red; } +.cgreen { color: $gl-text-green; } .cdark { color: #444 } /** COMMON CLASSES **/ diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index e14203f827..741ff9051a 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -60,17 +60,17 @@ } .new-file { a { - color: $gl-success; + color: $gl-text-green; } } .renamed-file { a { - color: $gl-warning; + color: $gl-text-orange; } } .deleted-file { a { - color: $gl-danger; + color: $gl-text-red; } } .edit-file{ From 0d967bce574c3750429bf2aed7f8c4ddc6ce1971 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 13:52:08 +0200 Subject: [PATCH 117/543] Show additions/deletions stats on merge request diff Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/views/projects/diffs/_diffs.html.haml | 9 ++++--- app/views/projects/diffs/_stats.html.haml | 30 +++++++++++------------ lib/gitlab/diff/file.rb | 8 ++++++ lib/gitlab/diff/line.rb | 8 ++++++ 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 981a93a240..0264dd2a33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ v 8.1.0 (unreleased) - Fix bug when removed file was not appearing in merge request diff - Note the original location of a moved project when notifying users of the move - Improve error message when merging fails + - Show additions/deletions stats on merge request diff v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index c5acafa263..ac2461b3b2 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,14 +1,17 @@ - if params[:view] == 'parallel' - fluid_layout true +- diff_files = safe_diff_files(diffs) + .gray-content-block.second-block .inline-parallel-buttons .btn-group = inline_diff_btn = parallel_diff_btn - = render 'projects/diffs/stats', diffs: diffs - -- diff_files = safe_diff_files(diffs) + - additions = diff_files.sum(&:added_lines) + - deletions = diff_files.sum(&:removed_lines) + = render 'projects/diffs/stats', diff_files: diff_files, + additions: additions, deletions: deletions - if diff_files.count < diffs.size = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index c4d7f26430..9e49716f7a 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,37 +2,35 @@ .commit-stat-summary Showing = link_to '#', class: 'js-toggle-button' do - %strong #{pluralize(diffs.count, "changed file")} - - if current_controller?(:commit) - - unless @commit.has_zero_stats? - with - %strong.cgreen #{@commit.stats.additions} additions - and - %strong.cred #{@commit.stats.deletions} deletions + %strong #{pluralize(diff_files.count, "changed file")} + with + %strong.cgreen #{additions} additions + and + %strong.cred #{deletions} deletions .file-stats.js-toggle-content.hide %ul - - diffs.each_with_index do |diff, i| + - diff_files.each_with_index do |diff_file, i| %li - - if diff.deleted_file + - if diff_file.deleted_file %span.deleted-file %a{href: "#diff-#{i}"} %i.fa.fa-minus - = diff.old_path - - elsif diff.renamed_file + = diff_file.old_path + - elsif diff_file.renamed_file %span.renamed-file %a{href: "#diff-#{i}"} %i.fa.fa-minus - = diff.old_path + = diff_file.old_path → - = diff.new_path - - elsif diff.new_file + = diff_file.new_path + - elsif diff_file.new_file %span.new-file %a{href: "#diff-#{i}"} %i.fa.fa-plus - = diff.new_path + = diff_file.new_path - else %span.edit-file %a{href: "#diff-#{i}"} %i.fa.fa-adjust - = diff.new_path + = diff_file.new_path diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 4daf65331e..142058aa69 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -44,6 +44,14 @@ module Gitlab diff.old_path end end + + def added_lines + diff_lines.select(&:added?).size + end + + def removed_lines + diff_lines.select(&:removed?).size + end end end end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 8ac1b15e88..0072194606 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -7,6 +7,14 @@ module Gitlab @text, @type, @index = text, type, index @old_pos, @new_pos = old_pos, new_pos end + + def added? + type == 'new' + end + + def removed? + type == 'old' + end end end end From efeeed2a0a1798a8d5ad4bfd014f6f3f4d9f3d8c Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 1 Oct 2015 14:06:29 +0200 Subject: [PATCH 118/543] content block height fix --- app/assets/stylesheets/generic/sidebar.scss | 46 ++++++++++++++++++++- app/views/layouts/_page.html.haml | 2 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index c5ea3aca7c..39532f2899 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -18,15 +18,20 @@ } .content-wrapper { - min-height: 100vh; + min-height: 900px; + display: table; width: 100%; padding: 20px; background: #EAEBEC; + height: 100%; + width: 100%; .container-fluid { background: #FFF; padding: $gl-padding; - min-height: 90vh; + /*min-height: 90vh;*/ + height: 100%; + min-height: 100%; &.container-blank { background: none; @@ -36,6 +41,43 @@ } } + +.content { + height: 100%; + width: 100%; + +} + +.max_height { + height: 100%; + display: table; + width: 100%; +} + +.project-show-readme { + height: 100%; + display: table-row; +} + +.wiki { + min-width: 1167px; +} + +section { + height: 100%; + display: table-row; +} + +html, body { + height: 100%; + margin: 0; +} + +.page-with-sidebar{ + min-height: 100%; + height: 100%; +} + .nav-sidebar { margin-top: 14 + $header-height; margin-bottom: 100px; diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 2468687b56..95a6267e2e 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,5 +25,5 @@ = render "layouts/flash" %div{ class: container_class } .content - .clearfix + .clearfix.max_height = yield From 4c43d846f2c414143a149509961de68b6e341371 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 14:12:43 +0200 Subject: [PATCH 119/543] Fix 500 error on compare page Signed-off-by: Dmitriy Zaporozhets --- app/helpers/diff_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 9d718e13b8..b896fba370 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -170,7 +170,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file - @merge_request ? @merge_request.commits.last : @commit.parent_id + @merge_request ? @merge_request.commits.last : @commit.parents.first else @commit end From a4383aea4ce5d610509d0019b85dabe90695deec Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 1 Oct 2015 14:18:12 +0200 Subject: [PATCH 120/543] css aligning --- app/assets/stylesheets/generic/sidebar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index 39532f2899..e3a5b7ad65 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -29,7 +29,6 @@ .container-fluid { background: #FFF; padding: $gl-padding; - /*min-height: 90vh;*/ height: 100%; min-height: 100%; From 2fa8820671eb68d7888be3382703ed95b0b14b65 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 1 Oct 2015 15:05:13 +0200 Subject: [PATCH 121/543] Golang download instructions assume amd64 --- doc/install/installation.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/install/installation.md b/doc/install/installation.md index 518b914fe6..3c62b11988 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -131,6 +131,9 @@ Install the Bundler Gem: Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server. This is a small daemon written in Go. To install gitlab-git-http-server we need a Go compiler. +The instructions below assume you use 64-bit Linux. You can find +downloads for other platforms at the [Go download +page](https://golang.org/dl). curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \ From e2576d588fef876e743f55b0b5b0620da43ab1c1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 15:51:28 +0200 Subject: [PATCH 122/543] Avoid unnecessary usage of local vars Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/diffs/_diffs.html.haml | 5 +---- app/views/projects/diffs/_stats.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index ac2461b3b2..4f1965bfb3 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -8,10 +8,7 @@ .btn-group = inline_diff_btn = parallel_diff_btn - - additions = diff_files.sum(&:added_lines) - - deletions = diff_files.sum(&:removed_lines) - = render 'projects/diffs/stats', diff_files: diff_files, - additions: additions, deletions: deletions + = render 'projects/diffs/stats', diff_files: diff_files - if diff_files.count < diffs.size = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 9e49716f7a..ea2a3e0127 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -4,9 +4,9 @@ = link_to '#', class: 'js-toggle-button' do %strong #{pluralize(diff_files.count, "changed file")} with - %strong.cgreen #{additions} additions + %strong.cgreen #{diff_files.sum(&:added_lines)} additions and - %strong.cred #{deletions} deletions + %strong.cred #{diff_files.sum(&:removed_lines)} deletions .file-stats.js-toggle-content.hide %ul - diff_files.each_with_index do |diff_file, i| From c5280434399ee489eebda254b2d246252df68f2b Mon Sep 17 00:00:00 2001 From: Cristian Bica Date: Thu, 1 Oct 2015 17:05:20 +0300 Subject: [PATCH 123/543] 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 9441f2dd0abd5db343b552787dbaa69f93a9a529 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 16:34:41 +0200 Subject: [PATCH 124/543] Add CHANGELOG item Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index c6eba9c873..6729a3bb04 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.1.0 (unreleased) - Fix bug when removed file was not appearing in merge request diff - 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) v 8.0.3 - Fix URL shown in Slack notifications From e2108f16ec2c8bcc51a7afe69bfe20f3bb0cb37f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 29 Sep 2015 09:37:46 -0700 Subject: [PATCH 125/543] Update upgrade guide for Redis >= 2.4.0 requirement [ci skip] Closes #2837 --- README.md | 2 +- doc/update/7.14-to-8.0.md | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99d5bc0b6c..e66a5546db 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 - Git 1.7.10+ -- Redis 2.0+ +- Redis 2.4+ - MySQL or PostgreSQL For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html). diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 552216be93..b5d7382e49 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -175,12 +175,21 @@ Also note that because Apache does not support upstreams behind Unix sockets you 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. Start application +### 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 -### 11. Check application status +### 12. Check application status Check if GitLab and its environment are configured correctly: From e3bc0e210451494ad5290755a79a510ea5c9a18b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 1 Oct 2015 18:10:09 +0200 Subject: [PATCH 126/543] Ensure GitLab CI project exists when CI service is activated manually When I check activeated checkbox in project services for GitLab CI it cause half-working state when gitlab_ci_project is missing. This patch fixes it until we have proper behaviour implemented later Signed-off-by: Dmitriy Zaporozhets --- app/models/project_services/gitlab_ci_service.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 436d4cfed8..a095eaaada 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -22,12 +22,17 @@ class GitlabCiService < CiService include Gitlab::Application.routes.url_helpers after_save :compose_service_hook, if: :activated? + after_save :ensure_gitlab_ci_project, if: :activated? def compose_service_hook hook = service_hook || build_service_hook hook.save end + def ensure_gitlab_ci_project + project.ensure_gitlab_ci_project + end + def supported_events %w(push tag_push) end From a3c6ed5c4650a7c8643e57929a34a219c6a15a8a Mon Sep 17 00:00:00 2001 From: SAKATA Sinji Date: Thu, 1 Oct 2015 13:09:07 +0900 Subject: [PATCH 127/543] Fix link with emoji --- app/helpers/gitlab_markdown_helper.rb | 2 +- spec/helpers/gitlab_markdown_helper_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 153a44870f..12b87dca79 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -21,7 +21,7 @@ module GitlabMarkdownHelper gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user) - fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) + fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' # Fragment has only one node, and it's a link generated by `gfm`. # Replace it with our requested link. diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index be0e0c747b..20ae29e2bd 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -106,6 +106,12 @@ describe GitlabMarkdownHelper do act = link_to_gfm(text, '/foo') expect(act).to eq %Q(#{issues[0].to_reference}) end + + it 'should replace commit message with emoji to link' do + actual = link_to_gfm(':book:Book', '/foo') + expect(actual). + to eq %Q(:book:Book) + end end describe '#render_wiki_content' do From d8bc6423121174ebe8145e90f511ad8cc88dc1c6 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 23 Sep 2015 16:00:23 +0200 Subject: [PATCH 128/543] Remove email footer text --- CHANGELOG | 1 + app/views/layouts/notify.html.haml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 33451e51ad..9a269652f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.1.0 (unreleased) - Improve error message when merging fails - Add support of multibyte characters in LDAP UID (Roman Petrov) - Show additions/deletions stats on merge request diff + - Remove footer text in emails (Zeger-Jan van de Weg) v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index ec209c38ee..2f7d7e86f5 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -42,5 +42,3 @@ - else #{link_to "View it on GitLab", @target_url} = email_action @target_url - - if @project && !@disable_footer - You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. From 788a3f9b94f5500bbbe3f3c4955c6c41196a736e Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 15 Sep 2015 15:33:44 -0500 Subject: [PATCH 129/543] Add last push widget to starred projects dashboard --- app/controllers/dashboard/projects_controller.rb | 1 + app/views/dashboard/projects/starred.html.haml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 467d0f81ac..58e9049f15 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -20,6 +20,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = current_user.starred_projects @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) + @last_push = current_user.recent_push @groups = [] respond_to do |format| diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 339362701d..f75f2e0a32 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -3,6 +3,9 @@ = render 'dashboard/projects_head' +- if @last_push + = render "events/event_last_push", event: @last_push + - if @projects.any? = render 'projects' - else From b35dd6b908c790d2724f0147d3af28dbae0a4a52 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 17:08:22 -0400 Subject: [PATCH 130/543] Update CHANGELOG [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 492e4b9aeb..aa8c146a6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,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 + - Ensure code blocks are properly highlighted after a note is updated v 8.0.3 - Fix URL shown in Slack notifications From 19748ddee6ef4c794d6cc30cdf1c607088cc5bf7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 21:38:39 -0400 Subject: [PATCH 131/543] Update config/locales/devise.en.yml with latest version It looks like a lot of changes but it's not, they just sorted it alphabetically. --- config/locales/devise.en.yml | 103 ++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index d8bf0878a3..bd4c3ebc69 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -1,61 +1,62 @@ -# Additional translations at http://github.com/plataformatec/devise/wiki/I18n +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" expired: "has expired, please request a new one" not_found: "not found" - already_confirmed: "was already confirmed, please try signing in" not_locked: "was not locked" not_saved: one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:" - - devise: - failure: - already_authenticated: 'You are already signed in.' - unauthenticated: 'You need to sign in before continuing.' - unconfirmed: 'You have to confirm your account before continuing.' - locked: 'Your account is locked.' - not_found_in_database: 'Invalid email or password.' - invalid: 'Invalid email or password.' - invalid_token: 'Invalid authentication token.' - timeout: 'Your session expired, please sign in again to continue.' - inactive: 'Your account was not activated yet.' - sessions: - signed_in: '' - signed_out: '' - users_sessions: - user: - signed_in: 'Signed in successfully.' - passwords: - send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' - updated: 'Your password was changed successfully. You are now signed in.' - updated_not_active: 'Your password was changed successfully.' - send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." - no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - confirmations: - send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' - send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.' - confirmed: 'Your account was successfully confirmed. You are now signed in.' - registrations: - signed_up: 'Welcome! You have signed up successfully.' - updated: 'You updated your account successfully.' - destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' - signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' - signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' - signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' - unlocks: - send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' - unlocked: 'Your account was successfully unlocked. You are now signed in.' - send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.' - omniauth_callbacks: - success: 'Successfully authorized from %{kind} account.' - failure: 'Could not authorize you from %{kind} because "%{reason}".' - mailer: - confirmation_instructions: - subject: 'Confirmation instructions' - reset_password_instructions: - subject: 'Reset password instructions' - unlock_instructions: - subject: 'Unlock Instructions' From ad7ad8745a33581680091c5ade9377c0aae74715 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 21:41:56 -0400 Subject: [PATCH 132/543] Add User#recently_sent_password_reset? --- app/models/user.rb | 4 ++++ spec/models/user_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 3879f3fd38..6a1e5fd52e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -327,6 +327,10 @@ class User < ActiveRecord::Base @reset_token end + def recently_sent_password_reset? + reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago + end + def disable_two_factor! update_attributes( two_factor_enabled: false, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 480950859a..b45c78f38d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -227,6 +227,26 @@ describe User do end end + describe 'recently_sent_password_reset?' do + it 'is false when reset_password_sent_at is nil' do + user = build_stubbed(:user, reset_password_sent_at: nil) + + expect(user.recently_sent_password_reset?).to eq false + end + + it 'is false when sent more than one minute ago' do + user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago) + + expect(user.recently_sent_password_reset?).to eq false + end + + it 'is true when sent less than one minute ago' do + user = build_stubbed(:user, reset_password_sent_at: Time.now) + + expect(user.recently_sent_password_reset?).to eq true + end + end + describe '#disable_two_factor!' do it 'clears all 2FA-related fields' do user = create(:user, :two_factor) From c7b43126bd7f5ef1b76a546029754ee44d68288e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 21:46:51 -0400 Subject: [PATCH 133/543] Add recently_reset message to Devise translations --- config/locales/devise.en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index bd4c3ebc69..22070e37f0 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -30,6 +30,7 @@ en: success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password has been changed successfully. You are now signed in." From b8ff38b1d47e4323f799b593b95821ed4a8c11f7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 21:47:27 -0400 Subject: [PATCH 134/543] Refactor PasswordsController to use before_actions --- app/controllers/passwords_controller.rb | 42 ++++++++++++------------- spec/features/password_reset_spec.rb | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index a2d152addc..2025158d06 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,21 +1,7 @@ class PasswordsController < Devise::PasswordsController - - def create - email = resource_params[:email] - self.resource = resource_class.find_by_email(email) - - if resource && resource.ldap_user? - flash[:alert] = "Cannot reset password for LDAP user." - respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return - end - - unless can_send_reset_email? - flash[:alert] = "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again." - respond_with({}, location: new_password_path(resource_name)) and return - end - - super - end + before_action :resource_from_email, only: [:create] + before_action :prevent_ldap_reset, only: [:create] + before_action :throttle_reset, only: [:create] def edit super @@ -37,10 +23,24 @@ class PasswordsController < Devise::PasswordsController end end - private + protected - def can_send_reset_email? - resource && (resource.reset_password_sent_at.blank? || - resource.reset_password_sent_at < 1.minute.ago) + def resource_from_email + email = resource_params[:email] + self.resource = resource_class.find_by_email(email) + end + + def prevent_ldap_reset + return unless resource && resource.ldap_user? + + redirect_to after_sending_reset_password_instructions_path_for(resource_name), + alert: "Cannot reset password for LDAP user." + end + + def throttle_reset + return unless resource && resource.recently_sent_password_reset? + + redirect_to new_password_path(resource_name), + alert: I18n.t('devise.passwords.recently_reset') end end diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 4d512c6543..85e70b4d47 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -29,7 +29,7 @@ feature 'Password reset', feature: true do visit root_path forgot_password(user) - expect(page).to have_content("Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again.") + expect(page).to have_content(I18n.t('devise.passwords.recently_reset')) expect(current_path).to eq new_user_password_path end end From f036d4095e5700a1663ce3429b0342fa813c3c7a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 1 Oct 2015 23:46:43 -0400 Subject: [PATCH 135/543] Fix spec broken by updated Devise translations --- spec/features/login_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index cef432e512..922c76285d 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -95,7 +95,7 @@ feature 'Login', feature: true do user = create(:user, password: 'not-the-default') login_with(user) - expect(page).to have_content('Invalid email or password.') + expect(page).to have_content('Invalid login or password.') end end end From d40dd5cfe331c5e465b77c8eecae9697c873a67a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 2 Oct 2015 00:14:47 -0400 Subject: [PATCH 136/543] Conform to spec guidelines that only exist in my head [ci skip] --- spec/models/user_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b45c78f38d..5e6918a5ac 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -227,7 +227,7 @@ describe User do end end - describe 'recently_sent_password_reset?' do + describe '#recently_sent_password_reset?' do it 'is false when reset_password_sent_at is nil' do user = build_stubbed(:user, reset_password_sent_at: nil) From 5de0b078442da2adc2b0673e3286c7d1a7cb2501 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 2 Oct 2015 10:08:16 +0200 Subject: [PATCH 137/543] Prevent creating 2 Ci::Project entities when enable CI Signed-off-by: Dmitriy Zaporozhets --- app/models/project.rb | 6 +----- app/services/git_push_service.rb | 2 +- features/steps/project/commits/commits.rb | 2 +- features/steps/shared/project.rb | 2 +- spec/models/project_spec.rb | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 8527fa2980..fa7690d8fd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -751,13 +751,9 @@ class Project < ActiveRecord::Base gitlab_ci_project || create_gitlab_ci_project end - def enable_ci(user) - # Enable service + def enable_ci service = gitlab_ci_service || create_gitlab_ci_service service.active = true service.save - - # Create Ci::Project - Ci::CreateProjectService.new.execute(user, self) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 8193b6e192..f9a8265d2d 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -58,7 +58,7 @@ class GitPushService # If CI was disabled but .gitlab-ci.yml file was pushed # we enable CI automatically if !project.gitlab_ci? && gitlab_ci_yaml?(newrev) - project.enable_ci(user) + project.enable_ci end EventCreateService.new.push(project, user, @push_data) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 47f58091b9..5ebc3a4976 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -103,7 +103,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'commit has ci status' do - @project.enable_ci(@user) + @project.enable_ci create :ci_commit, gl_project: @project, sha: sample_commit.id end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index fc51cec150..5744e455eb 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -199,7 +199,7 @@ module SharedProject step 'project "Shop" has CI enabled' do project = Project.find_by(name: "Shop") - project.enable_ci(@user) + project.enable_ci end step 'project "Shop" has CI build' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fe7bb2cc13..1c3f5374a2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -417,9 +417,8 @@ describe Project do describe :enable_ci do let(:project) { create :project } - let(:user) { create :user } - before { project.enable_ci(user) } + before { project.enable_ci } it { expect(project.gitlab_ci?).to be_truthy } it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } From 37e9e71ea1162fbae13bdc9c41684bdd4ad03b1e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 2 Oct 2015 10:26:56 +0200 Subject: [PATCH 138/543] Remove unnecessary fork ci logic Signed-off-by: Dmitriy Zaporozhets --- .../project_services/gitlab_ci_service.rb | 15 -------- app/services/ci/create_project_service.rb | 30 --------------- app/services/projects/fork_service.rb | 8 +++- .../ci/create_project_service_spec.rb | 37 ------------------- spec/services/projects/fork_service_spec.rb | 2 +- 5 files changed, 8 insertions(+), 84 deletions(-) delete mode 100644 app/services/ci/create_project_service.rb delete mode 100644 spec/services/ci/create_project_service_spec.rb diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index a095eaaada..6d2cf79b69 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -72,21 +72,6 @@ class GitlabCiService < CiService :error end - def fork_registration(new_project, current_user) - params = OpenStruct.new({ - id: new_project.id, - default_branch: new_project.default_branch - }) - - ci_project = Ci::Project.find_by!(gitlab_id: project.id) - - Ci::CreateProjectService.new.execute( - current_user, - params, - ci_project - ) - end - def commit_coverage(sha, ref) get_ci_commit(sha, ref).coverage rescue ActiveRecord::RecordNotFound diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb deleted file mode 100644 index f42babd238..0000000000 --- a/app/services/ci/create_project_service.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Ci - class CreateProjectService - include Gitlab::Application.routes.url_helpers - - def execute(current_user, params, forked_project = nil) - @project = Ci::Project.parse(params) - - Ci::Project.transaction do - @project.save! - - gl_project = ::Project.find(@project.gitlab_id) - gl_project.build_missing_services - gl_project.gitlab_ci_service.update_attributes(active: true) - end - - if forked_project - # Copy settings - settings = forked_project.attributes.select do |attr_name, value| - ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name - end - - @project.update(settings) - end - - Ci::EventService.new.create_project(current_user, @project) - - @project - end - end -end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 2e995d6fd5..46374a3909 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -18,7 +18,13 @@ module Projects if new_project.persisted? if @project.gitlab_ci? - @project.gitlab_ci_service.fork_registration(new_project, @current_user) + new_project.enable_ci + + settings = @project.gitlab_ci_project.attributes.select do |attr_name, value| + ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name + end + + new_project.gitlab_ci_project.update(settings) end end diff --git a/spec/services/ci/create_project_service_spec.rb b/spec/services/ci/create_project_service_spec.rb deleted file mode 100644 index 2de7b0deca..0000000000 --- a/spec/services/ci/create_project_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateProjectService do - let(:service) { Ci::CreateProjectService.new } - let(:current_user) { double.as_null_object } - let(:project) { FactoryGirl.create :project } - - describe :execute do - context 'valid params' do - subject { service.execute(current_user, project) } - - it { is_expected.to be_kind_of(Ci::Project) } - it { is_expected.to be_persisted } - end - - context 'without project dump' do - it 'should raise exception' do - expect { service.execute(current_user, '', '') }. - to raise_error(NoMethodError) - end - end - - context "forking" do - let(:ci_origin_project) do - FactoryGirl.create(:ci_project, shared_runners_enabled: true, public: true, allow_git_fetch: true) - end - - subject { service.execute(current_user, project, ci_origin_project) } - - it "uses project as a template for settings and jobs" do - expect(subject.shared_runners_enabled).to be_truthy - expect(subject.public).to be_truthy - expect(subject.allow_git_fetch).to be_truthy - end - end - end -end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 18ab333c1d..a850aa7cbd 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -48,7 +48,7 @@ describe Projects::ForkService do @from_project.build_missing_services @from_project.gitlab_ci_service.update_attributes(active: true) - expect_any_instance_of(Ci::CreateProjectService).to receive(:execute) + expect_any_instance_of(Project).to receive(:enable_ci) fork_project(@from_project, @to_user) end From f012e07c4ffe21bf5f2661b5a7837261cbe1106c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 2 Oct 2015 08:46:39 +0000 Subject: [PATCH 139/543] Document known issue with Reply by email and multiple application servers. --- doc/incoming_email/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 01ab22321e..dc5bfeb290 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -2,6 +2,10 @@ GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. +**Warning**: Do not enable Reply by email if you have **multiple GitLab application servers**. +Due to an issue with the way incoming emails are read from the mail server, every incoming reply-by-email email will result in as many comments being created as you have application servers. +A fix is being worked on. + ## 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. @@ -204,4 +208,4 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. bundle exec rake gitlab:incoming_email:check RAILS_ENV=development ``` -8. Reply by email should now be working. +8. Reply by email should now be working. \ No newline at end of file From 3515cb9b2de4b6c1018fec35b0513eb7dc33dc66 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 2 Oct 2015 11:02:05 +0200 Subject: [PATCH 140/543] Fix tests Signed-off-by: Dmitriy Zaporozhets --- .../gitlab_ci_service_spec.rb | 21 ------------------- spec/services/projects/fork_service_spec.rb | 12 ++++------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 8cdd551a0c..989cfe0916 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -56,25 +56,4 @@ describe GitlabCiService do end end end - - describe "Fork registration" do - before do - @old_project = create(:ci_project).gl_project - @project = create(:empty_project) - @user = create(:user) - - @service = GitlabCiService.new - allow(@service).to receive_messages( - service_hook: true, - project_url: 'http://ci.gitlab.org/projects/2', - token: 'verySecret', - project: @old_project - ) - end - - it "creates fork on CI" do - expect_any_instance_of(Ci::CreateProjectService).to receive(:execute) - @service.fork_registration(@project, @user) - end - end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index a850aa7cbd..65a8c81204 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -43,14 +43,10 @@ describe Projects::ForkService do end context 'GitLab CI is enabled' do - it "calls fork registrator for CI" do - create(:ci_project, gl_project: @from_project) - @from_project.build_missing_services - @from_project.gitlab_ci_service.update_attributes(active: true) - - expect_any_instance_of(Project).to receive(:enable_ci) - - fork_project(@from_project, @to_user) + it "fork and enable CI for fork" do + @from_project.enable_ci + @to_project = fork_project(@from_project, @to_user) + expect(@to_project.gitlab_ci?).to be_truthy end end end From fd86b66914c1fff35d38ed0715d64f7e9dfa2757 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 2 Oct 2015 11:24:12 +0200 Subject: [PATCH 141/543] CSS markup fixed Everything is fixed according DZ comments. Added a bit sexy transition for our project buttons >< --- app/assets/stylesheets/base/layout.scss | 6 ++++ app/assets/stylesheets/generic/buttons.scss | 4 +++ app/assets/stylesheets/generic/sidebar.scss | 40 ++------------------- app/assets/stylesheets/pages/projects.scss | 16 +++++++++ app/views/layouts/_page.html.haml | 2 +- 5 files changed, 30 insertions(+), 38 deletions(-) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index b91c15d891..ced3769af0 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -1,15 +1,21 @@ html { overflow-y: scroll; + height: 100%; + margin: 0; &.touch .tooltip { display: none !important; } body { padding-top: $header-height; + height: 100%; + margin: 0; } } .container { padding-top: 0; + height: 100%; + width: 100%; z-index: 5; } diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index cf76f538e0..a5fe589044 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -94,6 +94,7 @@ body { @mixin btn-info { @include border-radius(2px); + @include transition (all 0.2s ease 0s); border-width: 1px; border-style: solid; @@ -116,6 +117,7 @@ body { &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; border-style: solid; } @@ -123,6 +125,7 @@ body { @mixin btn-middle { @include border-radius(2px); + @include transition (all 0.2s ease 0s); border-width: 1px; border-style: solid; @@ -145,6 +148,7 @@ body { &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; border-style: solid; } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index e3a5b7ad65..d30fc6e189 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -1,4 +1,7 @@ .page-with-sidebar { + min-height: 100%; + height: 100%; + .sidebar-wrapper { position: fixed; top: 0; @@ -40,43 +43,6 @@ } } - -.content { - height: 100%; - width: 100%; - -} - -.max_height { - height: 100%; - display: table; - width: 100%; -} - -.project-show-readme { - height: 100%; - display: table-row; -} - -.wiki { - min-width: 1167px; -} - -section { - height: 100%; - display: table-row; -} - -html, body { - height: 100%; - margin: 0; -} - -.page-with-sidebar{ - min-height: 100%; - height: 100%; -} - .nav-sidebar { margin-top: 14 + $header-height; margin-bottom: 100px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a5940543a9..c1505b9a62 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -505,3 +505,19 @@ pre.light-well { display: inline-block; } +.content { + height: 100%; + width: 100%; + +} + +.max-height { + height: 100%; + display: table; + width: 100%; +} + +section { + height: 100%; + display: table-row; +} \ No newline at end of file diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 95a6267e2e..1f4ade81ed 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,5 +25,5 @@ = render "layouts/flash" %div{ class: container_class } .content - .clearfix.max_height + .clearfix.max-height = yield From 2fa89a3dc6a7580969203e43808048b79f172c0c Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 2 Oct 2015 11:29:46 +0200 Subject: [PATCH 142/543] Added benchmark-ips to the Gemfile This allows me to use this Gem for benchmarking without having to add/remove it every time. --- Gemfile | 2 ++ Gemfile.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index f1674c560e..4938cbf8b8 100644 --- a/Gemfile +++ b/Gemfile @@ -270,6 +270,8 @@ group :development, :test do gem 'rubocop', '~> 0.28.0', require: false gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.10.0', require: false + + gem 'benchmark-ips', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 6472221bc5..1dd56cd9c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,6 +66,7 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) bcrypt (3.1.10) + benchmark-ips (2.3.0) better_errors (1.0.1) coderay (>= 1.0.0) erubis (>= 2.6.6) @@ -795,6 +796,7 @@ DEPENDENCIES asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) + benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.0) From 731b860976772111ca673ba25aeadd3634adbe7d Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 1 Oct 2015 17:33:05 +0300 Subject: [PATCH 143/543] Hide password in the service settings form --- CHANGELOG | 1 + app/controllers/projects/services_controller.rb | 4 +++- app/views/shared/_field.html.haml | 7 +++++-- features/project/service.feature | 1 + features/steps/project/services.rb | 4 ++++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a8b43dd460..8d95dd79af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.1.0 (unreleased) - Show additions/deletions stats on merge request diff - Remove footer text in emails (Zeger-Jan van de Weg) - Ensure code blocks are properly highlighted after a note is updated + - Hide password in the service settings form v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 3a22ed832a..3047ee8a1f 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -58,6 +58,8 @@ class Projects::ServicesController < Projects::ApplicationController end def service_params - params.require(:service).permit(ALLOWED_PARAMS) + service_params = params.require(:service).permit(ALLOWED_PARAMS) + service_params.delete("password") if service_params["password"].blank? + service_params end end diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index 45ec49280d..8d6e16f74c 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -8,7 +8,10 @@ - help = field[:help] .form-group - = form.label name, title, class: "control-label" + - if type == "password" && value.present? + = form.label name, "Change #{title}", class: "control-label" + - else + = form.label name, title, class: "control-label" .col-sm-10 - if type == 'text' = form.text_field name, class: "form-control", placeholder: placeholder @@ -19,6 +22,6 @@ - elsif type == 'select' = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' - = form.password_field name, value: value, class: 'form-control' + = form.password_field name, autocomplete: "new-password", class: 'form-control' - if help %span.help-block= help diff --git a/features/project/service.feature b/features/project/service.feature index fdff640ec8..5014b52b9f 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -72,6 +72,7 @@ Feature: Project Services And I click Atlassian Bamboo CI service link And I fill Atlassian Bamboo CI settings Then I should see Atlassian Bamboo CI service settings saved + And I should see empty field Change Password Scenario: Activate jetBrains TeamCity CI service When I visit project "Shop" services page diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index d3b462bfd3..1c700df0c6 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -202,6 +202,10 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Username').value).to eq 'user' end + step 'I should see empty field Change Password' do + expect(find_field('Change Password').value).to be_nil + end + step 'I click JetBrains TeamCity CI service link' do click_link 'JetBrains TeamCity CI' end From 5ebcf21b81eaf3b67d1f7c675ec54be19041a379 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 2 Oct 2015 12:01:46 +0200 Subject: [PATCH 144/543] section and .content now in layout.cssc --- app/assets/stylesheets/base/layout.scss | 10 ++++++++++ app/assets/stylesheets/pages/projects.scss | 11 ----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index ced3769af0..c6301ab6d3 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -19,6 +19,16 @@ html { z-index: 5; } +.content { + height: 100%; + width: 100%; +} + +.content section { + height: 100%; + display: table-row; +} + .container .content { margin: 0 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c1505b9a62..ddcf65d38f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -505,19 +505,8 @@ pre.light-well { display: inline-block; } -.content { - height: 100%; - width: 100%; - -} - .max-height { height: 100%; display: table; width: 100%; -} - -section { - height: 100%; - display: table-row; } \ No newline at end of file From a4292066f0b8dc7f0ff69b3f7a6a11424a42ad01 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 2 Oct 2015 12:44:20 +0200 Subject: [PATCH 145/543] Back and forth permission on builds/ --- doc/migrate_ci_to_ce/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 46ce0fe98c..1cb1bc2e76 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -182,6 +182,7 @@ will need this file later. ``` # On your CI server: # Omnibus +sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds sudo gitlab-ci-rake backup:create # Source @@ -228,6 +229,7 @@ be no CI data yet because you turned CI on the GitLab server off earlier. ``` # On your GitLab server: # Omnibus +sudo chown git:git /var/opt/gitlab/gitlab-ci/builds sudo gitlab-rake ci:migrate # Source @@ -342,7 +344,12 @@ The fix for this is to update to Omnibus 7.14 first and then update it to 8.0. #### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds To fix that issue you have to change builds/ folder permission before doing final backup: ``` -chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds +sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds +``` + +Then before executing `ci:migrate` you need to fix builds folder permission: +``` +sudo chown git:git /var/opt/gitlab/gitlab-ci/builds ``` #### Problems when importing CI database to GitLab From 97e6c9b42ca65be14f64f1528821922b2b0bd04a Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 2 Oct 2015 15:10:33 +0300 Subject: [PATCH 146/543] Wrong access level badge on MR comments --- CHANGELOG | 1 + app/models/project_team.rb | 4 ++++ app/views/projects/notes/_note.html.haml | 6 +++--- spec/models/project_team_spec.rb | 12 ++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a8b43dd460..d0bc80daaf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.1.0 (unreleased) - Show additions/deletions stats on merge request diff - Remove footer text in emails (Zeger-Jan van de Weg) - Ensure code blocks are properly highlighted after a note is updated + - Fix wrong access level badge on MR comments v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 56e49af232..f602a96536 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -135,6 +135,10 @@ class ProjectTeam !!find_member(user_id) end + def human_max_access(user_id) + Gitlab::Access.options.key max_member_access(user_id) + end + def max_member_access(user_id) access = [] access << project.project_members.find_by(user_id: user_id).try(:access_field) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index cf5d5d6d8b..1638ad6891 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -14,10 +14,10 @@ = icon('trash-o') - unless note.system - - member = note.project.team.find_member(note.author.id) - - if member + - access = note.project.team.human_max_access(note.author.id) + - if access %span.note-role.label - = member.human_access + = access = link_to_member(note.project, note.author, avatar: false) diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index cc1138490a..26e8fdae47 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -66,4 +66,16 @@ describe ProjectTeam do it { expect(project.team.member?(guest)).to be_truthy } end end + + describe "#human_max_access" do + it "return master role" do + user = create :user + group = create :group + group.add_users([user.id], GroupMember::MASTER) + project = create(:project, namespace: group) + project.team << [user, :guest] + + expect(project.team.human_max_access(user.id)).to eq("Master") + end + end end From dbc05d4a62468a8b7ebbeb17da4f74edaa09f968 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 2 Oct 2015 16:25:47 +0200 Subject: [PATCH 147/543] Don't use "rm" for cleaning tmp/builds If this directory were to be empty this would result in warnings being printed to STDERR, cluttering spec output. Doing this in Ruby fixes this problem (and also removes the need for shell alltogether). --- spec/support/setup_builds_storage.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb index a3e5964618..a4f21e9533 100644 --- a/spec/support/setup_builds_storage.rb +++ b/spec/support/setup_builds_storage.rb @@ -10,8 +10,10 @@ RSpec.configure do |config| end config.after(:suite) do - Dir.chdir(builds_path) do - `ls | grep -v .gitkeep | xargs rm -r` + Dir[File.join(builds_path, '*')].each do |path| + next if File.basename(path) == '.gitkeep' + + FileUtils.rm_rf(path) end end end From acdb5f34cffc124ac9f727ff434f375b0bbe9971 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 2 Oct 2015 16:38:37 +0200 Subject: [PATCH 148/543] max height to layout.scss --- app/assets/stylesheets/base/layout.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index c6301ab6d3..f0569a5e67 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -40,3 +40,9 @@ html { .container-limited { max-width: $fixed-layout-width; } + +.max-height { + height: 100%; + display: table; + width: 100%; +} \ No newline at end of file From 4a157e50a2083570c36542cc4ddae9a53f13575f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 2 Oct 2015 14:40:40 +0000 Subject: [PATCH 149/543] Add link to mail_room issue. [ci skip] --- doc/incoming_email/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index dc5bfeb290..316746ab54 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -4,7 +4,7 @@ GitLab can be set up to allow users to comment on issues and merge requests by r **Warning**: Do not enable Reply by email if you have **multiple GitLab application servers**. Due to an issue with the way incoming emails are read from the mail server, every incoming reply-by-email email will result in as many comments being created as you have application servers. -A fix is being worked on. +[A fix is being worked on.](https://github.com/tpitale/mail_room/issues/46) ## Get a mailbox From 19893a1c10e4e6dfbdb56ad78de1599b6c8f6981 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 2 Oct 2015 17:00:23 +0200 Subject: [PATCH 150/543] Basic setup for an RSpec based benchmark suite This benchmark suite uses benchmark-ips (https://github.com/evanphx/benchmark-ips) behind the scenes. Specs can be turned into benchmark specs by setting "benchmark" to "true" in the top-level describe block like so: describe SomeClass, benchmark: true do end Writing benchmarks can be done using custom RSpec matchers, for example: describe MaruTheCat, benchmark: true do describe '#jump_in_box' do it 'should run 1000 iterations per second' do maru = described_class.new expect { maru.jump_in_box }.to iterate_per_second(1000) end end end By default the "iterate_per_second" expectation requires a standard deviation under 30% (this is just an arbitrary default for now). You can change this by chaining "with_maximum_stddev" on the expectation: expect { maru.jump_in_box }.to iterate_per_second(1000) .with_maximum_stddev(10) This will change the expectation to require a maximum deviation of 10%. Alternatively you can use the it block style to write specs: describe MaruTheCat, benchmark: true do describe '#jump_in_box' do subject { -> { described_class.new } } it { is_expected.to iterate_per_second(1000) } end end Because "iterate_per_second" operates on a block, opposed to a static value, the "subject" method must return a Proc. This looks a bit goofy but I have been unable to find a nice way around this. --- .gitlab-ci.yml | 7 ++++ lib/tasks/spec.rake | 11 +++++- spec/benchmarks/models/user_spec.rb | 40 ++++++++++++++++++++ spec/spec_helper.rb | 3 +- spec/support/matchers/benchmark_matchers.rb | 42 +++++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 spec/benchmarks/models/user_spec.rb create mode 100644 spec/support/matchers/benchmark_matchers.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddf4e31204..a9aaf82ec1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,13 @@ spec:api: - ruby - mysql +spec:benchmark: + script: + - RAILS_ENV=test bundle exec rake spec:benchmark + tags: + - ruby + - mysql + spec:other: script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 831746815d..3ae5c25069 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -19,11 +19,20 @@ namespace :spec do run_commands(cmds) end + desc 'GitLab | Rspec | Run benchmark specs' + task :benchmark do + cmds = [ + %W(rake gitlab:setup), + %W(rspec spec --tag @benchmark) + ] + run_commands(cmds) + end + desc 'GitLab | Rspec | Run other specs' task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api --tag ~@feature) + %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark) ] run_commands(cmds) end diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb new file mode 100644 index 0000000000..a08c84ffce --- /dev/null +++ b/spec/benchmarks/models/user_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe User, benchmark: true do + describe '.by_login' do + before do + %w{Alice Bob Eve}.each do |name| + create(:user, + email: "#{name}@gitlab.com", + username: name, + name: name) + end + end + + let(:iterations) { 1000 } + + describe 'using a capitalized username' do + subject { -> { User.by_login('Alice') } } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a lowercase username' do + subject { -> { User.by_login('alice') } } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a capitalized Email address' do + subject { -> { User.by_login('Alice@gitlab.com') } } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a lowercase Email address' do + subject { -> { User.by_login('alice@gitlab.com') } } + + it { is_expected.to iterate_per_second(iterations) } + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dfe855926c..05bb32baa9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' +require 'benchmark/ips' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -32,7 +33,7 @@ RSpec.configure do |config| config.include TestEnv config.include StubGitlabCalls config.include StubGitlabData - + config.include BenchmarkMatchers, benchmark: true config.infer_spec_type_from_file_location! config.raise_errors_for_deprecations! diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb new file mode 100644 index 0000000000..45a1d49345 --- /dev/null +++ b/spec/support/matchers/benchmark_matchers.rb @@ -0,0 +1,42 @@ +module BenchmarkMatchers + extend RSpec::Matchers::DSL + + matcher :iterate_per_second do |min_iterations| + supports_block_expectations + + match do |block| + @max_stddev ||= 30 + + @entry = benchmark(&block) + + expect(@entry.ips).to be >= min_iterations + expect(@entry.stddev_percentage).to be <= @max_stddev + end + + chain :with_maximum_stddev do |value| + @max_stddev = value + end + + description do + "run at least #{min_iterations} iterations per second" + end + + failure_message do + ips = @entry.ips.round(2) + stddev = @entry.stddev_percentage.round(2) + + "expected at least #{min_iterations} iterations per second " \ + "with a maximum stddev of #{@max_stddev}%, instead of " \ + "#{ips} iterations per second with a stddev of #{stddev}%" + end + end + + # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry. + def benchmark(&block) + report = Benchmark.ips(quiet: true) do |bench| + bench.report(&block) + end + + report.entries[0] + end +end From 75c03530f8c85924d0b7dfc020ebe65ec0488dba Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 2 Oct 2015 17:08:12 +0200 Subject: [PATCH 151/543] removed max-heght from project.scss --- app/assets/stylesheets/pages/projects.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ddcf65d38f..acf0744012 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -503,10 +503,4 @@ pre.light-well { .inline-form { display: inline-block; -} - -.max-height { - height: 100%; - display: table; - width: 100%; } \ No newline at end of file From 42be5ee1cd8eb752c52ddb82bdf7c452190d4193 Mon Sep 17 00:00:00 2001 From: Ricardo Band Date: Sat, 12 Sep 2015 20:53:18 +0000 Subject: [PATCH 152/543] hooks: Add full project namespace to payload Payload of "project_member, :create" and "project_member, :destroy" now also have a field project_path_with_namespace. --- CHANGELOG | 1 + app/services/system_hooks_service.rb | 1 + doc/system_hooks/system_hooks.md | 42 +++++++++++----------- spec/services/system_hooks_service_spec.rb | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f01da3fe7d..01772e1d33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.1.0 (unreleased) - Ensure code blocks are properly highlighted after a note is updated - Fix wrong access level badge on MR comments - Hide password in the service settings form + - Add full project namespace to payload of system webhooks (Ricardo Band) v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 60235b6be2..9a5fe4af9d 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -54,6 +54,7 @@ class SystemHooksService data.merge!({ project_name: model.project.name, project_path: model.project.path, + project_path_with_namespace: model.project.path_with_namespace, project_id: model.project.id, user_name: model.user.name, user_email: model.user.email, diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index b0e4613cde..5cb05b13b3 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -48,16 +48,17 @@ X-Gitlab-Event: System Hook ```json { - "created_at": "2012-07-21T07:30:56Z", - "event_name": "user_add_to_team", - "project_access": "Master", - "project_id": 74, - "project_name": "StoreCloud", - "project_path": "storecloud", - "user_email": "johnsmith@gmail.com", - "user_name": "John Smith", - "user_id": 41, - "project_visibility": "private", + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_add_to_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41, + "project_visibility": "private", } ``` @@ -65,16 +66,17 @@ X-Gitlab-Event: System Hook ```json { - "created_at": "2012-07-21T07:30:56Z", - "event_name": "user_remove_from_team", - "project_access": "Master", - "project_id": 74, - "project_name": "StoreCloud", - "project_path": "storecloud", - "user_email": "johnsmith@gmail.com", - "user_name": "John Smith", - "user_id": 41, - "project_visibility": "private", + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_remove_from_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41, + "project_visibility": "private", } ``` diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 48c49e2f71..a31fc1e4b0 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -13,8 +13,8 @@ describe SystemHooksService do it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } From dbc85bfa01d9ee37e466bd537a20f1cfde89be9f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 1 Oct 2015 22:11:10 -0700 Subject: [PATCH 153/543] Fix bug where transferring a project would result in stale commit links Transferring a project to another namespace updates the project's updated_at field, but since the cache key did not depend on the object, the page fragments were not invalidated. This resulted in stale links to the commits. Changing the cache key to use the object pathname solves this issue. Closes gitlab-org/omnibus-gitlab#843 --- CHANGELOG | 1 + app/views/projects/commits/_commit.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f01da3fe7d..ec23d0f117 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 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) - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) - Add user preference to view activities as default dashboard (Stan Hu) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index efad4cb147..cddd5aa3a8 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,7 +5,7 @@ - note_count = notes.user.count - ci_commit = project.ci_commit(commit.sha) -- cache_key = [project.id, commit.id, note_count] +- cache_key = [project.path_with_namespace, commit.id, note_count] - cache_key.push(ci_commit.status) if ci_commit = cache(cache_key) do From 3fbcc51102ad12948efe7d0fd1d69c1150aba416 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 2 Oct 2015 13:03:51 -0700 Subject: [PATCH 154/543] Update README cache key to use full project namespace --- app/helpers/projects_helper.rb | 2 +- spec/helpers/projects_helper_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7b4747ce3d..a0220af4c3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -296,7 +296,7 @@ module ProjectsHelper def readme_cache_key sha = @project.commit.try(:sha) || 'nil' - [@project.id, sha, "readme"].join('-') + [@project.path_with_namespace, sha, "readme"].join('-') end def round_commit_count(project) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 99abb95d90..53e56ebff4 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -61,13 +61,13 @@ describe ProjectsHelper do end it "returns a valid cach key" do - expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme") + expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-#{project.commit.id}-readme") end it "returns a valid cache key if HEAD does not exist" do allow(project).to receive(:commit) { nil } - expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme") + expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-nil-readme") end end end From 0406455c8a9094d7849f586a473cf6f5d3253f10 Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 00:56:16 -0500 Subject: [PATCH 155/543] Enable "UselessAssignment" rubocop lint --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 05b8ecc3b0..11e4502849 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -932,7 +932,7 @@ Lint/UselessAccessModifier: Lint/UselessAssignment: Description: 'Checks for useless assignment to a local variable.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' - Enabled: false + Enabled: true Lint/UselessComparison: Description: 'Checks for comparison of something with itself.' From 2b075f16c7e867ae19a79fb7c59ca6754f9df7bf Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 00:56:37 -0500 Subject: [PATCH 156/543] Fix rubocop warnings in app --- app/controllers/ci/lints_controller.rb | 2 +- .../omniauth_callbacks_controller.rb | 4 +-- app/controllers/projects/wikis_controller.rb | 2 +- app/finders/issuable_finder.rb | 2 +- app/helpers/application_helper.rb | 2 +- app/models/ci/build.rb | 6 ++--- app/models/ci/project.rb | 2 +- app/models/merge_request_diff.rb | 8 +++--- app/models/project.rb | 4 +-- .../project_services/hipchat_service.rb | 25 ++++++++----------- app/models/repository.rb | 2 +- app/models/user.rb | 8 +----- app/services/files/base_service.rb | 2 +- app/services/projects/create_service.rb | 2 +- 14 files changed, 29 insertions(+), 42 deletions(-) diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index a81e4e319f..24dd1b5c93 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -18,7 +18,7 @@ module Ci rescue Ci::GitlabCiYamlProcessor::ValidationError => e @error = e.message @status = false - rescue Exception => e + rescue Exception @error = "Undefined error" @status = false end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 523264b8ea..f809fa7500 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -71,7 +71,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end - rescue Gitlab::OAuth::SignupDisabledError => e + rescue Gitlab::OAuth::SignupDisabledError label = Gitlab::OAuth::Provider.label_for(oauth['provider']) message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." @@ -80,7 +80,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end flash[:notice] = message - + redirect_to new_user_session_path end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 51c26a6a46..88fccfed50 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -98,7 +98,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - rescue ProjectWiki::CouldNotCreateWikiError => ex + rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) return false diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index ab89aa2c53..6aa16673d6 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -39,7 +39,7 @@ class IssuableFinder items = by_assignee(items) items = by_author(items) items = by_label(items) - items = sort(items) + sort(items) end def group diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 39ab83ccf1..3ab44719d9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -35,7 +35,7 @@ module ApplicationHelper def project_icon(project_id, options = {}) project = if project_id.is_a?(Project) - project = project_id + project_id else Project.find_with_namespace(project_id) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 19f957eb96..645ad68e1b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -26,7 +26,7 @@ module Ci class Build < ActiveRecord::Base extend Ci::Model - + LAZY_ATTRIBUTES = ['trace'] belongs_to :commit, class_name: 'Ci::Commit' @@ -140,7 +140,7 @@ module Ci def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? - html ||= '' + html || '' end def started? @@ -211,7 +211,7 @@ module Ci if coverage.present? coverage.to_f end - rescue => ex + rescue # if bad regex or something goes wrong we dont want to interrupt transition # so we just silentrly ignore error for now end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb index 24f7017109..88ba933a43 100644 --- a/app/models/ci/project.rb +++ b/app/models/ci/project.rb @@ -184,7 +184,7 @@ module Ci # If service is available but missing in db # we should create an instance. Ex `create_gitlab_ci_service` - service = self.send :"create_#{service_name}_service" if service.nil? + self.send :"create_#{service_name}_service" if service.nil? end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index f75f999b0d..c9ef8023ae 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -144,12 +144,10 @@ class MergeRequestDiff < ActiveRecord::Base # Collect array of Git::Diff objects # between target and source branches def unmerged_diffs - diffs = compare_result.diffs - diffs ||= [] - diffs - rescue Gitlab::Git::Diff::TimeoutError => ex + compare_result.diffs || [] + rescue Gitlab::Git::Diff::TimeoutError self.state = :timeout - diffs = [] + [] end def repository diff --git a/app/models/project.rb b/app/models/project.rb index 8527fa2980..06a66bbf49 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -413,7 +413,7 @@ class Project < ActiveRecord::Base if template.nil? # If no template, we should create an instance. Ex `create_gitlab_ci_service` - service = self.send :"create_#{service_name}_service" + self.send :"create_#{service_name}_service" else Service.create_from_template(self.id, template) end @@ -738,7 +738,7 @@ class Project < ActiveRecord::Base def create_wiki ProjectWiki.new(self, self.owner).wiki true - rescue ProjectWiki::CouldNotCreateWikiError => ex + rescue ProjectWiki::CouldNotCreateWikiError errors.add(:base, 'Failed create wiki') false end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 7a15a861ab..af2840a57f 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -85,17 +85,16 @@ class HipchatService < Service def create_message(data) object_kind = data[:object_kind] - message = \ - case object_kind - when "push", "tag_push" - create_push_message(data) - when "issue" - create_issue_message(data) unless is_update?(data) - when "merge_request" - create_merge_request_message(data) unless is_update?(data) - when "note" - create_note_message(data) - end + case object_kind + when "push", "tag_push" + create_push_message(data) + when "issue" + create_issue_message(data) unless is_update?(data) + when "merge_request" + create_merge_request_message(data) unless is_update?(data) + when "note" + create_note_message(data) + end end def create_push_message(push) @@ -167,8 +166,6 @@ class HipchatService < Service obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) merge_request_id = obj_attr[:iid] - source_branch = obj_attr[:source_branch] - target_branch = obj_attr[:target_branch] state = obj_attr[:state] description = obj_attr[:description] title = obj_attr[:title] @@ -194,8 +191,6 @@ class HipchatService < Service data = HashWithIndifferentAccess.new(data) user_name = data[:user][:name] - repo_attr = HashWithIndifferentAccess.new(data[:repository]) - obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) note = obj_attr[:note] note_url = obj_attr[:url] diff --git a/app/models/repository.rb b/app/models/repository.rb index 79b48ebfed..2c5ab62d22 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -549,7 +549,7 @@ class Repository # Run GitLab post receive hook post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo) - status = post_receive_hook.trigger(gl_id, oldrev, newrev, ref) + post_receive_hook.trigger(gl_id, oldrev, newrev, ref) else # Remove tmp ref and return error to user rugged.references.delete(tmp_ref) diff --git a/app/models/user.rb b/app/models/user.rb index 9ea7cabff1..ef687f737c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -701,13 +701,7 @@ class User < ActiveRecord::Base end def manageable_namespaces - @manageable_namespaces ||= - begin - namespaces = [] - namespaces << namespace - namespaces += owned_groups - namespaces += masters_groups - end + @manageable_namespaces ||= [namespace] + owned_groups + masters_groups end def namespaces diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 7aecee217d..008833eed8 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -21,7 +21,7 @@ module Files create_target_branch end - if sha = commit + if commit success else error("Something went wrong. Your changes were not committed") diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index e54a13ed6c..faf1ee008e 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -62,7 +62,7 @@ module Projects after_create_actions if @project.persisted? @project - rescue => ex + rescue @project.errors.add(:base, "Can't save project. Please try again later") @project end From 59d0263bc86962737def7d2174e302a312d96574 Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 01:29:58 -0500 Subject: [PATCH 157/543] Fix rubocop warnings in lib --- lib/api/helpers.rb | 5 ++--- lib/event_filter.rb | 2 +- lib/gitlab/contributions_calendar.rb | 1 - lib/gitlab/diff/parser.rb | 2 -- lib/gitlab/fogbugz_import/project_creator.rb | 2 +- lib/gitlab/google_code_import/project_creator.rb | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7fada98fcd..549b1f9e9a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -63,11 +63,11 @@ module API user_project.build_missing_services service_method = "#{underscored_service}_service" - + send_service(service_method) end end - + @project_service || not_found!("Service") end @@ -149,7 +149,6 @@ module API end def attributes_for_keys(keys, custom_params = nil) - params_hash = custom_params || params attrs = {} keys.each do |key| if params[key].present? or (params.has_key?(key) and params[key] == false) diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 163937c02c..f15b2cfd23 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -47,7 +47,7 @@ class EventFilter actions << Event::COMMENTED if filter.include? 'comments' - events = events.where(action: actions) + events.where(action: actions) end def options(key) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 45bb904ed7..8a7f8dc500 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -12,7 +12,6 @@ module Gitlab @timestamps = {} date_from = 1.year.ago - date_to = Date.today events = Event.reorder(nil).contributions.where(author_id: user.id). where("created_at > ?", date_from).where(project_id: projects). diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index c1d9520ddf..7015fe36c3 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -14,8 +14,6 @@ module Gitlab lines_arr = ::Gitlab::InlineDiff.processing lines lines_arr.each do |line| - raw_line = line.dup - next if filename?(line) full_line = html_escape(line.gsub(/\n/, '')) diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index f02ea43910..8b1b6f48ed 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -23,7 +23,7 @@ module Gitlab import_url: Project::UNKNOWN_IMPORT_URL ).execute - import_data = project.create_import_data( + project.create_import_data( data: { 'repo' => repo.raw_data, 'user_map' => user_map, diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 0cfeaf9d61..1cb7d16aeb 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -23,7 +23,7 @@ module Gitlab import_url: repo.import_url ).execute - import_data = project.create_import_data( + project.create_import_data( data: { "repo" => repo.raw_data, "user_map" => user_map From 848d7b2a2be28982d0f164d4e3a966c300e7c322 Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 01:48:54 -0500 Subject: [PATCH 158/543] Fix rubocop warnings in spec/models --- spec/models/broadcast_message_spec.rb | 4 ++-- spec/models/ci/commit_spec.rb | 4 ++-- spec/models/hooks/project_hook_spec.rb | 4 ++-- spec/models/hooks/service_hook_spec.rb | 2 -- spec/models/hooks/web_hook_spec.rb | 2 -- spec/models/milestone_spec.rb | 6 +++--- spec/models/project_services/gitlab_ci_service_spec.rb | 1 - spec/models/project_services/hipchat_service_spec.rb | 4 ++-- spec/models/project_spec.rb | 2 +- spec/models/project_wiki_spec.rb | 2 +- spec/models/wiki_page_spec.rb | 2 +- 11 files changed, 14 insertions(+), 19 deletions(-) diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 8ab72151a6..d80748f23a 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -27,12 +27,12 @@ describe BroadcastMessage do end it "should return nil if time not come" do - broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) + create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do - broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) + create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) expect(BroadcastMessage.current).to be_nil end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 5429151c8d..c04bbcbadc 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -228,13 +228,13 @@ describe Ci::Commit do it "returns finished_at of latest build" do build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60 - build1 = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120 + FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120 expect(commit.finished_at.to_i).to eq(build.finished_at.to_i) end it "returns nil if there is no finished build" do - build = FactoryGirl.create :ci_not_started_build, commit: commit + FactoryGirl.create :ci_not_started_build, commit: commit expect(commit.finished_at).to be_nil end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index dae7e399cf..a2dc66fce3 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -22,7 +22,7 @@ describe ProjectHook do describe '.push_hooks' do it 'should return hooks for push events only' do hook = create(:project_hook, push_events: true) - hook2 = create(:project_hook, push_events: false) + create(:project_hook, push_events: false) expect(ProjectHook.push_hooks).to eq([hook]) end end @@ -30,7 +30,7 @@ describe ProjectHook do describe '.tag_push_hooks' do it 'should return hooks for tag push events only' do hook = create(:project_hook, tag_push_events: true) - hook2 = create(:project_hook, tag_push_events: false) + create(:project_hook, tag_push_events: false) expect(ProjectHook.tag_push_hooks).to eq([hook]) end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 4c8b8910ae..16641c1212 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -39,8 +39,6 @@ describe ServiceHook do end it "POSTs the data as JSON" do - json = @data.to_json - @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 23f30881d9..2fdc49f02e 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -60,8 +60,6 @@ describe ProjectHook do end it "POSTs the data as JSON" do - json = @data.to_json - @project_hook.execute(@data, 'push_hooks') expect(WebMock).to have_requested(:post, @project_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 36352e1ecc..c88d534966 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -111,8 +111,8 @@ describe Milestone do describe :is_empty? do before do - issue = create :closed_issue, milestone: milestone - merge_request = create :merge_request, milestone: milestone + create :closed_issue, milestone: milestone + create :merge_request, milestone: milestone end it 'Should return total count of issues and merge requests assigned to milestone' do @@ -125,7 +125,7 @@ describe Milestone do milestone = create :milestone create :closed_issue, milestone: milestone - issue = create :issue + create :issue end it 'should be true if milestone active and all nested issues closed' do diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 8cdd551a0c..ab5d0ccd3c 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -49,7 +49,6 @@ describe GitlabCiService do let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } it "calls ci_yaml_file" do - service_hook = double expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha]) @service.execute(push_sample_data) diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 65d16beef9..f67d7b3098 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -87,7 +87,7 @@ describe HipchatService do it "should create a push message" do message = hipchat.send(:create_push_message, push_sample_data) - obj_attr = push_sample_data[:object_attributes] + push_sample_data[:object_attributes] branch = push_sample_data[:ref].gsub('refs/heads/', '') expect(message).to include("#{user.name} pushed to branch " \ "#{branch} of " \ @@ -107,7 +107,7 @@ describe HipchatService do it "should create a tag push message" do message = hipchat.send(:create_push_message, push_sample_data) - obj_attr = push_sample_data[:object_attributes] + push_sample_data[:object_attributes] expect(message).to eq("#{user.name} pushed new tag " \ "test to " \ "#{project_name}\n") diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fe7bb2cc13..3df5300fe3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -140,7 +140,7 @@ describe Project do describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do - last_activity_event = create(:event, project: project) + create(:event, project: project) expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i) end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f785203af7..94802dcfb7 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -231,7 +231,7 @@ describe ProjectWiki do end def commit_details - commit = { name: user.name, email: user.email, message: "test commit" } + { name: user.name, email: user.email, message: "test commit" } end def create_page(name, content) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index dc84a14bb4..d7802d1734 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -196,7 +196,7 @@ describe WikiPage do end def commit_details - commit = { name: user.name, email: user.email, message: "test commit" } + { name: user.name, email: user.email, message: "test commit" } end def create_page(name, content) From 5792eb954433a661bf1cf6ef17f849058dca5bc7 Mon Sep 17 00:00:00 2001 From: Geoffrey Challen Date: Sat, 3 Oct 2015 08:58:31 -0400 Subject: [PATCH 159/543] Fix referals for :back and relative URL installs. --- app/views/layouts/_head.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index c3b137e3dd..74174a72f5 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -3,7 +3,7 @@ %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} %meta{content: "GitLab Community Edition", name: "description"} - %meta{name: 'referrer', content: 'origin'} + %meta{name: 'referrer', content: 'origin-when-cross-origin'} %title= page_title From aed145a9c68f5130a8e46602c4b6f1b76c8c13bf Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 16:02:21 -0500 Subject: [PATCH 160/543] Fix rubocop warnings in spec/lib and spec/tasks --- spec/lib/gitlab/backend/grack_auth_spec.rb | 4 ++-- spec/lib/gitlab/o_auth/user_spec.rb | 4 ---- spec/tasks/gitlab/backup_rake_spec.rb | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index 829a9c197e..37c527221a 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -151,14 +151,14 @@ describe Grack::Auth do end it "repeated attempts followed by successful attempt" do - for n in 0..maxretry do + maxretry.times.each do expect(attempt_login(false)).to eq(401) end expect(attempt_login(true)).to eq(200) expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey - for n in 0..maxretry do + maxretry.times.each do expect(attempt_login(false)).to eq(401) end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index c0083fc85b..fd3ab1fb7c 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -19,10 +19,6 @@ describe Gitlab::OAuth::User do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do - # FIXME (rspeicher): It's unlikely that this test is actually doing anything - # `auth` is never used and removing it entirely doesn't break the test, so - # what's it doing? - auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') expect( oauth_user.persisted? ).to be_truthy end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 2e63e5f36a..3be7dd4e52 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -159,7 +159,7 @@ describe 'gitlab:app namespace rake task' do end it "does not contain skipped item" do - tar_contents, exit_status = Gitlab::Popen.popen( + tar_contents, _exit_status = Gitlab::Popen.popen( %W{tar -tvf #{@backup_tar} db uploads repositories builds} ) From 963e6366be870bc78037424a5e9d150f342dcfac Mon Sep 17 00:00:00 2001 From: Guilherme Garnier Date: Sat, 3 Oct 2015 19:59:54 -0500 Subject: [PATCH 161/543] Fix rubocop warnings in features --- features/steps/groups.rb | 50 ++++++++++++------------- features/steps/project/graph.rb | 2 +- features/steps/project/issues/issues.rb | 4 +- features/steps/project/redirects.rb | 2 - features/steps/shared/group.rb | 2 +- spec/features/ci/admin/builds_spec.rb | 8 ++-- 6 files changed, 33 insertions(+), 35 deletions(-) diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 95bc9baf8d..69ddfa42c0 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -282,9 +282,9 @@ class Spinach::Features::Groups < Spinach::FeatureSteps milestone1_project2 = create :milestone, title: "Version 7.2", project: project2 - milestone1_project3 = create :milestone, - title: "Version 7.2", - project: @project3 + create :milestone, + title: "Version 7.2", + project: @project3 milestone2_project1 = create :milestone, title: "GL-113", project: @project1 @@ -301,28 +301,28 @@ class Spinach::Features::Groups < Spinach::FeatureSteps assignee: current_user, author: current_user, milestone: milestone2_project1 - issue2 = create :issue, - project: project2, - assignee: current_user, - author: current_user, - milestone: milestone1_project2 - issue3 = create :issue, - project: @project3, - assignee: current_user, - author: current_user, - milestone: milestone1_project1 - mr1 = create :merge_request, - source_project: @project1, - target_project: @project1, - assignee: current_user, - author: current_user, - milestone: milestone2_project1 - mr2 = create :merge_request, - source_project: project2, - target_project: project2, - assignee: current_user, - author: current_user, - milestone: milestone2_project2 + create :issue, + project: project2, + assignee: current_user, + author: current_user, + milestone: milestone1_project2 + create :issue, + project: @project3, + assignee: current_user, + author: current_user, + milestone: milestone1_project1 + create :merge_request, + source_project: @project1, + target_project: @project1, + assignee: current_user, + author: current_user, + milestone: milestone2_project1 + create :merge_request, + source_project: project2, + target_project: project2, + assignee: current_user, + author: current_user, + milestone: milestone2_project2 @mr3 = create :merge_request, source_project: @project3, target_project: @project3, diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 9453d63644..4abd5288d5 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -32,6 +32,6 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps end def project - project ||= Project.find_by(name: "Shop") + @project ||= Project.find_by(name: "Shop") end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 239392eab9..af2da41bad 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -223,11 +223,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do - issue = create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project) + create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project) end step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do - issue = create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project) + create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project) end step 'I fill in issue search with \'Description for issue1\'' do diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 0e724138a8..1ffd5cb9de 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -39,7 +39,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'Authenticate' do admin = create(:admin) - project = Project.find_by(name: 'Community') fill_in "user_login", with: admin.email fill_in "user_password", with: admin.password click_button "Sign in" @@ -54,7 +53,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I get redirected to signin page where I sign in' do admin = create(:admin) - project = Project.find_by(name: 'Enterprise') fill_in "user_login", with: admin.email fill_in "user_password", with: admin.password click_button "Sign in" diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 2d17fb34cc..83a0457697 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -37,7 +37,7 @@ module SharedGroup group = Group.find_by(name: groupname) || create(:group, name: groupname) group.add_user(user, role) project ||= create(:project, namespace: group, path: "project#{@project_count}") - event ||= create(:closed_issue_event, project: project) + create(:closed_issue_event, project: project) project.team << [user, :master] @project_count += 1 end diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb index ee757206a0..623d466c67 100644 --- a/spec/features/ci/admin/builds_spec.rb +++ b/spec/features/ci/admin/builds_spec.rb @@ -21,10 +21,10 @@ describe "Admin Builds" do describe "Tabs" do it "shows all builds" do - build = FactoryGirl.create :ci_build, commit: commit, status: "pending" - build1 = FactoryGirl.create :ci_build, commit: commit, status: "running" - build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" - build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" + FactoryGirl.create :ci_build, commit: commit, status: "pending" + FactoryGirl.create :ci_build, commit: commit, status: "running" + FactoryGirl.create :ci_build, commit: commit, status: "success" + FactoryGirl.create :ci_build, commit: commit, status: "failed" visit ci_admin_builds_path From 5e18a922e430de0d50344ce7f432ef9424e04de8 Mon Sep 17 00:00:00 2001 From: Lasse Schuirmann Date: Sun, 4 Oct 2015 13:49:48 +0000 Subject: [PATCH 162/543] Remove tmp/.gitkeep This file is obsolete since 5b4aba5f as it exists solely for the directory to exist. --- tmp/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tmp/.gitkeep diff --git a/tmp/.gitkeep b/tmp/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 From abf18abb52dbdb409d481854bde24ed880c76f56 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 4 Oct 2015 23:39:24 -0700 Subject: [PATCH 163/543] allow crawling of commit page but not patch/diffs Commit page has valuable information that search engines should be allowed to crawl however the .patch and .diff pages have no new information that is not on commit page --- public/robots.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/robots.txt b/public/robots.txt index 528f421083..4f616c7f4c 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -38,7 +38,8 @@ Disallow: /*/*/edit Disallow: /*/*/raw Disallow: /*/*/blame Disallow: /*/*/commits/*/* -Disallow: /*/*/commit +Disallow: /*/*/commit/*.patch +Disallow: /*/*/commit/*.diff Disallow: /*/*/compare Disallow: /*/*/branches/new Disallow: /*/*/tags/new From 546a3c6561fbe967cc37ccc3229b71893cd20c34 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 2 Oct 2015 13:46:38 +0200 Subject: [PATCH 164/543] Refactor commit and build --- app/controllers/ci/builds_controller.rb | 2 +- app/controllers/ci/commits_controller.rb | 6 +- app/controllers/ci/projects_controller.rb | 3 +- app/helpers/ci/commits_helper.rb | 2 +- app/helpers/ci/gitlab_helper.rb | 2 +- app/helpers/ci_status_helper.rb | 2 +- app/models/ci/build.rb | 16 ++ app/models/ci/commit.rb | 137 ++++++------------ app/models/ci/project_status.rb | 12 -- app/models/project.rb | 6 +- .../project_services/ci/hip_chat_message.rb | 2 +- .../project_services/ci/mail_service.rb | 2 +- .../project_services/ci/slack_message.rb | 2 +- .../project_services/gitlab_ci_service.rb | 4 +- app/services/ci/create_builds_service.rb | 27 ++++ app/services/ci/create_commit_service.rb | 39 ++--- .../ci/create_trigger_request_service.rb | 6 +- app/views/ci/builds/_build.html.haml | 4 + app/views/ci/builds/show.html.haml | 6 +- app/views/ci/commits/_commit.html.haml | 4 +- app/views/ci/commits/show.html.haml | 65 ++++----- .../ci/notify/build_fail_email.html.haml | 2 +- app/views/ci/notify/build_fail_email.text.erb | 2 +- .../ci/notify/build_success_email.html.haml | 2 +- .../ci/notify/build_success_email.text.erb | 2 +- config/routes.rb | 10 +- .../20151002112914_add_stage_idx_to_builds.rb | 5 + ...20151002121400_add_index_for_build_name.rb | 5 + ...0151002122929_add_sha_and_ref_to_builds.rb | 7 + ...1002122943_migrate_sha_and_ref_to_build.rb | 7 + lib/ci/gitlab_ci_yaml_processor.rb | 1 + spec/requests/ci/commits_spec.rb | 2 +- 32 files changed, 197 insertions(+), 197 deletions(-) create mode 100644 app/services/ci/create_builds_service.rb create mode 100644 db/migrate/20151002112914_add_stage_idx_to_builds.rb create mode 100644 db/migrate/20151002121400_add_index_for_build_name.rb create mode 100644 db/migrate/20151002122929_add_sha_and_ref_to_builds.rb create mode 100644 db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb index 80ee866679..bf87f81439 100644 --- a/app/controllers/ci/builds_controller.rb +++ b/app/controllers/ci/builds_controller.rb @@ -18,7 +18,7 @@ module Ci if commit # Redirect to commit page - redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha) + redirect_to ci_project_commit_path(@project, @build.commit) return end end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb index 7a0a500fbe..acf9189572 100644 --- a/app/controllers/ci/commits_controller.rb +++ b/app/controllers/ci/commits_controller.rb @@ -13,7 +13,7 @@ module Ci end def status - commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id], params[:ref_id]) render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) rescue ActiveRecord::RecordNotFound render json: { status: "not_found" } @@ -22,7 +22,7 @@ module Ci def cancel commit.builds.running_or_pending.each(&:cancel) - redirect_to ci_project_ref_commits_path(project, commit.ref, commit.sha) + redirect_to ci_project_commits_path(project, commit.sha) end private @@ -32,7 +32,7 @@ module Ci end def commit - @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) end end end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index e8788955eb..20e6c2c2ba 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -19,7 +19,8 @@ module Ci @ref = params[:ref] @commits = @project.commits.reverse_order - @commits = @commits.where(ref: @ref) if @ref + # TODO: this is broken + # @commits = @commits.where(ref: @ref) if @ref @commits = @commits.page(params[:page]).per(20) end diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb index 9069aed5b4..a0df4c3d72 100644 --- a/app/helpers/ci/commits_helper.rb +++ b/app/helpers/ci/commits_helper.rb @@ -1,7 +1,7 @@ module Ci module CommitsHelper def ci_commit_path(commit) - ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) + ci_project_commits_path(commit.project, commit) end def commit_link(commit) diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb index 13e4d0fd9c..baddbc806f 100644 --- a/app/helpers/ci/gitlab_helper.rb +++ b/app/helpers/ci/gitlab_helper.rb @@ -26,7 +26,7 @@ module Ci def yaml_web_editor_link(project) commits = project.commits - if commits.any? && commits.last.push_data[:ci_yaml_file] + if commits.any? && commits.last.ci_yaml_file "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" else "#{project.gitlab_url}/new/master" diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 3a88ed7107..794bdc2530 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,6 +1,6 @@ module CiStatusHelper def ci_status_path(ci_commit) - ci_project_ref_commits_path(ci_commit.project, ci_commit.ref, ci_commit) + ci_project_commits_path(ci_commit.project, ci_commit) end def ci_status_icon(ci_commit) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 645ad68e1b..bf3e891520 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -34,10 +34,12 @@ module Ci belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' serialize :options + serialize :push_data validates :commit, presence: true validates :status, presence: true validates :coverage, numericality: true, allow_blank: true + validates_presence_of :ref scope :running, ->() { where(status: "running") } scope :pending, ->() { where(status: "pending") } @@ -45,6 +47,9 @@ module Ci scope :failed, ->() { where(status: "failed") } scope :unstarted, ->() { where(runner_id: nil) } scope :running_or_pending, ->() { where(status:[:running, :pending]) } + scope :latest, ->() { group(:name).order(stage_idx: :asc, created_at: :desc) } + scope :ignore_failures, ->() { where(allow_failure: false) } + scope :for_ref, ->(ref) { where(ref: ref) } acts_as_taggable @@ -82,6 +87,7 @@ module Ci new_build.name = build.name new_build.allow_failure = build.allow_failure new_build.stage = build.stage + new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request new_build.save new_build @@ -187,6 +193,16 @@ module Ci project.name end + def project_recipients + recipients = project.email_recipients.split(' ') + + if project.email_add_pusher? && push_data[:user_email].present? + recipients << push_data[:user_email] + end + + recipients.uniq + end + def repo_url project.repo_url_with_auth end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 6d048779cd..35134b6628 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -23,9 +23,7 @@ module Ci has_many :builds, dependent: :destroy, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' - serialize :push_data - - validates_presence_of :ref, :sha, :before_sha, :push_data + validates_presence_of :sha validate :valid_commit_sha def self.truncate_sha(sha) @@ -69,15 +67,15 @@ module Ci end def git_author_name - commit_data[:author][:name] if commit_data && commit_data[:author] + commit_data.author_name if commit_data end def git_author_email - commit_data[:author][:email] if commit_data && commit_data[:author] + commit_data.author_email if commit_data end def git_commit_message - commit_data[:message] if commit_data && commit_data[:message] + commit_data.message if commit_data end def short_before_sha @@ -89,84 +87,31 @@ module Ci end def commit_data - push_data[:commits].find do |commit| - commit[:id] == sha - end + @commit ||= gl_project.commit(sha) rescue nil end - def project_recipients - recipients = project.email_recipients.split(' ') - - if project.email_add_pusher? && push_data[:user_email].present? - recipients << push_data[:user_email] - end - - recipients.uniq - end - def stage - return unless config_processor - stages = builds_without_retry.select(&:active?).map(&:stage) - config_processor.stages.find { |stage| stages.include? stage } + builds_without_retry.group(:stage_idx).select(:stage).last end - def create_builds_for_stage(stage, trigger_request) + def create_builds(ref, tag, push_data, trigger_request = nil) return if skip_ci? && trigger_request.blank? return unless config_processor - - builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) - builds_attrs.map do |build_attrs| - builds.create!({ - name: build_attrs[:name], - commands: build_attrs[:script], - tag_list: build_attrs[:tags], - options: build_attrs[:options], - allow_failure: build_attrs[:allow_failure], - stage: build_attrs[:stage], - trigger_request: trigger_request, - }) - end + CreateBuildsService.new.execute(self, config_processor, ref, tag, push_data, trigger_request) end - def create_next_builds(trigger_request) - return if skip_ci? && trigger_request.blank? - return unless config_processor - - stages = builds.where(trigger_request: trigger_request).group_by(&:stage) - - config_processor.stages.any? do |stage| - !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present? - end + def refs + builds.group(:ref).pluck(:ref) end - def create_builds(trigger_request = nil) - return if skip_ci? && trigger_request.blank? - return unless config_processor - - config_processor.stages.any? do |stage| - create_builds_for_stage(stage, trigger_request).present? - end + def last_ref + builds.latest.first.try(:ref) end def builds_without_retry - @builds_without_retry ||= - begin - grouped_builds = builds.group_by(&:name) - grouped_builds.map do |name, builds| - builds.sort_by(&:id).last - end - end - end - - def builds_without_retry_sorted - return builds_without_retry unless config_processor - - stages = config_processor.stages - builds_without_retry.sort_by do |build| - [stages.index(build.stage) || -1, build.name || ""] - end + builds.latest end def retried_builds @@ -180,35 +125,32 @@ module Ci return 'failed' elsif builds.none? return 'skipped' - elsif success? - 'success' - elsif pending? - 'pending' - elsif running? - 'running' - elsif canceled? - 'canceled' + end + + statuses = builds_without_retry.ignore_failures.pluck(:status) + if statuses.all? { |status| status == 'success' } + return 'success' + elsif statuses.all? { |status| status == 'pending' } + return 'pending' + elsif statuses.include?('running') || statuses.include?('pending') + return 'running' + elsif statuses.all? { |status| status == 'canceled' } + return 'canceled' else - 'failed' + return 'failed' end end def pending? - builds_without_retry.all? do |build| - build.pending? - end + status == 'pending' end def running? - builds_without_retry.any? do |build| - build.running? || build.pending? - end + status == 'running' end def success? - builds_without_retry.all? do |build| - build.success? || build.ignored? - end + status == 'success' end def failed? @@ -216,15 +158,17 @@ module Ci end def canceled? - builds_without_retry.all? do |build| - build.canceled? - end + status == 'canceled' end def duration @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i end + def duration_for_ref(ref) + builds_without_retry.for_ref(ref).select(&:duration).sum(&:duration).to_i + end + def finished_at @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) end @@ -238,12 +182,12 @@ module Ci end end - def matrix? - builds_without_retry.size > 1 + def matrix_for_ref?(ref) + builds_without_retry.for_ref(ref).pluck(:id).size > 1 end def config_processor - @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file]) + @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file) rescue Ci::GitlabCiYamlProcessor::ValidationError => e save_yaml_error(e.message) nil @@ -253,10 +197,15 @@ module Ci nil end + def ci_yaml_file + gl_project.repository.blob_at(sha, '.gitlab-ci.yml') + rescue + nil + end + def skip_ci? return false if builds.any? - commits = push_data[:commits] - commits.present? && commits.last[:message] =~ /(\[ci skip\])/ + git_commit_message =~ /(\[ci skip\])/ if git_commit_message end def update_committed! diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb index 6d5cafe81a..b66f1212f2 100644 --- a/app/models/ci/project_status.rb +++ b/app/models/ci/project_status.rb @@ -28,18 +28,6 @@ module Ci status end - # only check for toggling build status within same ref. - def last_commit_changed_status? - ref = last_commit.ref - last_commits = commits.where(ref: ref).last(2) - - if last_commits.size < 2 - false - else - last_commits[0].status != last_commits[1].status - end - end - def last_commit_for_ref(ref) commits.where(ref: ref).last end diff --git a/app/models/project.rb b/app/models/project.rb index b90a82da9f..bb47b9abb0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -744,7 +744,11 @@ class Project < ActiveRecord::Base end def ci_commit(sha) - gitlab_ci_project.commits.find_by(sha: sha) if gitlab_ci? + ci_commits.find_by(sha: sha) + end + + def ensure_ci_commit(sha) + ci_commit(sha) || ci_commits.create(sha: sha) end def ensure_gitlab_ci_project diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb index 25c72033ea..73fdbe801c 100644 --- a/app/models/project_services/ci/hip_chat_message.rb +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -13,7 +13,7 @@ module Ci lines.push("#{project.name} - ") if commit.matrix? - lines.push("Commit ##{commit.id}
") + lines.push("Commit ##{commit.id}
") else first_build = commit.builds_without_retry.first lines.push("Build '#{first_build.name}' ##{first_build.id}
") diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index 1bd2f33612..11a2743f96 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -61,7 +61,7 @@ module Ci end def execute(build) - build.commit.project_recipients.each do |recipient| + build.project_recipients.each do |recipient| case build.status.to_sym when :success mailer.build_success_email(build.id, recipient) diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb index 757b196114..7d254836fb 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -48,7 +48,7 @@ module Ci def attachment_message out = "<#{ci_project_url(project)}|#{project_name}>: " if commit.matrix? - out << "Commit <#{ci_project_ref_commits_url(project, commit.ref, commit.sha)}|\##{commit.id}> " + out << "Commit <#{ci_project_commits_url(project, commit.sha)}|\##{commit.id}> " else build = commit.builds_without_retry.first out << "Build <#{ci_project_build_url(project, build)}|\##{build.id}> " diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 6d2cf79b69..fd10851653 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -63,7 +63,7 @@ class GitlabCiService < CiService end def get_ci_commit(sha, ref) - Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha_and_ref!(sha, ref) + Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha!(sha) end def commit_status(sha, ref) @@ -80,7 +80,7 @@ class GitlabCiService < CiService def build_page(sha, ref) if project.gitlab_ci_project.present? - ci_project_ref_commits_url(project.gitlab_ci_project, ref, sha) + ci_project_commits_url(project.gitlab_ci_project, sha) end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb new file mode 100644 index 0000000000..e9c85410e5 --- /dev/null +++ b/app/services/ci/create_builds_service.rb @@ -0,0 +1,27 @@ +module Ci + class CreateBuildsService + def execute(commit, ref, tag, push_data, config_processor, trigger_request) + config_processor.stages.any? do |stage| + builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) + builds_attrs.map do |build_attrs| + # don't create the same build twice + unless commit.builds.find_by_name_and_trigger_request(name: build_attrs[:name], ref: ref, tag: tag, trigger_request: trigger_request) + commit.builds.create!({ + name: build_attrs[:name], + commands: build_attrs[:script], + tag_list: build_attrs[:tags], + options: build_attrs[:options], + allow_failure: build_attrs[:allow_failure], + stage: build_attrs[:stage], + stage_idx: build_attrs[:stage_idx], + trigger_request: trigger_request, + ref: ref, + tag: tag, + push_data: push_data, + }) + end + end + end + end + end +end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index 0a1abf89a9..9120a82edc 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -16,33 +16,22 @@ module Ci return false end - commit = project.commits.find_by_sha_and_ref(sha, ref) - - # Create commit if not exists yet - unless commit - data = { - ref: ref, - sha: sha, - tag: origin_ref.start_with?('refs/tags/'), - before_sha: before_sha, - push_data: { - before: before_sha, - after: sha, - ref: ref, - user_name: params[:user_name], - user_email: params[:user_email], - repository: params[:repository], - commits: params[:commits], - total_commits_count: params[:total_commits_count], - ci_yaml_file: params[:ci_yaml_file] - } - } - - commit = project.commits.create(data) - end + tag = origin_ref.start_with?('refs/tags/') + push_data = { + before: before_sha, + after: sha, + ref: ref, + user_name: params[:user_name], + user_email: params[:user_email], + repository: params[:repository], + commits: params[:commits], + total_commits_count: params[:total_commits_count], + ci_yaml_file: params[:ci_yaml_file] + } + commit = project.gl_project.ensure_ci_commit(sha) commit.update_committed! - commit.create_builds unless commit.builds.any? + commit.create_builds(ref, tag, push_data) commit end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 9bad09f2f5..f13ed787ed 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,15 +1,15 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.commits.where(ref: ref).last + commit = project.gl_project.commit(ref) return unless commit + ci_commit = project.gl_project.ensure_ci_commit(commit.sha) trigger_request = trigger.trigger_requests.create!( - commit: commit, variables: variables ) - if commit.create_builds(trigger_request) + if ci_commit.create_builds(ref, tag, nil, trigger_request) trigger_request end end diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml index 515b862e99..8ccc0dff2f 100644 --- a/app/views/ci/builds/_build.html.haml +++ b/app/views/ci/builds/_build.html.haml @@ -6,6 +6,10 @@ = link_to ci_project_build_path(build.project, build) do %strong Build ##{build.id} + - if defined?(ref) + %td + = build.ref + %td = build.stage diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml index 839dbf5c55..f941143e2e 100644 --- a/app/views/ci/builds/show.html.haml +++ b/app/views/ci/builds/show.html.haml @@ -1,7 +1,7 @@ #up-build-trace -- if @commit.matrix? +- if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu - - @commit.builds_without_retry_sorted.each do |build| + - @commit.builds_without_retry.for_ref(build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to ci_project_build_url(@project, build) do = ci_icon_for_status(build.status) @@ -12,7 +12,7 @@ = build.id - - unless @commit.builds_without_retry.include?(@build) + - unless @commit.builds_without_retry.for_ref(@build.ref).include?(@build) %li.active %a Build ##{@build.id} diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml index 1eacfca944..f8a1fa5085 100644 --- a/app/views/ci/commits/_commit.html.haml +++ b/app/views/ci/commits/_commit.html.haml @@ -7,7 +7,7 @@ %td.build-link - = link_to ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) do + = link_to ci_project_commits_path(commit.project, commit.sha) do %strong #{commit.short_sha} %td.build-message @@ -16,7 +16,7 @@ %td.build-branch - unless @ref %span - = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref) + = link_to truncate(commit.last_ref, length: 25), ci_project_path(@project, ref: commit.last_ref) %td.duration - if commit.duration > 0 diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml index 8f38aa8467..4badb67128 100644 --- a/app/views/ci/commits/show.html.haml +++ b/app/views/ci/commits/show.html.haml @@ -17,14 +17,11 @@ %p %span.attr-name Commit: #{gitlab_commit_link(@project, @commit.sha)} - - %p - %span.attr-name Branch: - #{gitlab_ref_link(@project, @commit.ref)} .col-sm-6 - %p - %span.attr-name Author: - #{@commit.git_author_name} (#{@commit.git_author_email}) + - if @commit.git_author_name || @commit.git_author_email + %p + %span.attr-name Author: + #{@commit.git_author_name} (#{@commit.git_author_email}) - if @commit.created_at %p %span.attr-name Created at: @@ -33,7 +30,7 @@ - if current_user && can?(current_user, :manage_builds, gl_project) .pull-right - if @commit.builds.running_or_pending.any? - = link_to "Cancel", cancel_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger' + = link_to "Cancel", cancel_ci_project_commits_path(@project, @commit), class: 'btn btn-sm btn-danger' - if @commit.yaml_errors.present? @@ -43,34 +40,17 @@ - @commit.yaml_errors.split(",").each do |error| %li= error -- unless @commit.push_data[:ci_yaml_file] +- unless @commit.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit -%h3 - Builds - - if @commit.duration > 0 - %small.pull-right - %i.fa.fa-time - #{time_interval_in_words @commit.duration} - -%table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Stage - %th Name - %th Duration - %th Finished at - - if @project.coverage_enabled? - %th Coverage - %th - = render @commit.builds_without_retry_sorted, controls: true - -- if @commit.retried_builds.any? +- @commit.refs.each do |ref| %h3 - Retried builds + Builds for #{ref} + - if @commit.duration_for_ref(ref) > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @commit.duration_for_ref(ref)} %table.table.builds %thead @@ -84,4 +64,23 @@ - if @project.coverage_enabled? %th Coverage %th - = render @commit.retried_builds + = render @commit.builds_without_retry.for_ref(ref), controls: true + +- if @commit.retried_builds.any? + %h3 + Retried builds + + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.retried_builds, ref: true diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml index d818e8b675..4ebdfa1b6c 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -11,7 +11,7 @@ %p Author: #{@build.commit.git_author_name} %p - Branch: #{@build.commit.ref} + Branch: #{@build.ref} %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 1add215a1c..177827f9a3 100644 --- a/app/views/ci/notify/build_fail_email.text.erb +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -3,7 +3,7 @@ Build failed for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> -Branch: <%= @build.commit.ref %> +Branch: <%= @build.ref %> Message: <%= @build.commit.git_commit_message %> Url: <%= ci_project_build_url(@build.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 a20dcaee24..7cc43300e8 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -12,7 +12,7 @@ %p Author: #{@build.commit.git_author_name} %p - Branch: #{@build.commit.ref} + Branch: #{@build.ref} %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 7ebd17e727..4d55c39b0f 100644 --- a/app/views/ci/notify/build_success_email.text.erb +++ b/app/views/ci/notify/build_success_email.text.erb @@ -3,7 +3,7 @@ Build successful for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> -Branch: <%= @build.commit.ref %> +Branch: <%= @build.ref %> Message: <%= @build.commit.git_commit_message %> Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/config/routes.rb b/config/routes.rb index 6d96d8801c..56087e489f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,12 +30,10 @@ Gitlab::Application.routes.draw do resource :charts, only: [:show] - resources :refs, constraints: { ref_id: /.*/ }, only: [] do - resources :commits, only: [:show] do - member do - get :status - get :cancel - end + resources :commits, only: [:show] do + member do + get :status + get :cancel end end diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb new file mode 100644 index 0000000000..68a745ffef --- /dev/null +++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb @@ -0,0 +1,5 @@ +class AddStageIdxToBuilds < ActiveRecord::Migration + def change + add_column :ci_builds, :stage_idx, :integer + end +end diff --git a/db/migrate/20151002121400_add_index_for_build_name.rb b/db/migrate/20151002121400_add_index_for_build_name.rb new file mode 100644 index 0000000000..c6a81d7466 --- /dev/null +++ b/db/migrate/20151002121400_add_index_for_build_name.rb @@ -0,0 +1,5 @@ +class AddIndexForBuildName < ActiveRecord::Migration + def up + add_index :ci_builds, [:commit_id, :stage_idx, :created_at] + end +end diff --git a/db/migrate/20151002122929_add_sha_and_ref_to_builds.rb b/db/migrate/20151002122929_add_sha_and_ref_to_builds.rb new file mode 100644 index 0000000000..fc367341f1 --- /dev/null +++ b/db/migrate/20151002122929_add_sha_and_ref_to_builds.rb @@ -0,0 +1,7 @@ +class AddShaAndRefToBuilds < ActiveRecord::Migration + def change + add_column :ci_builds, :tag, :boolean + add_column :ci_builds, :ref, :string + add_column :ci_builds, :push_data, :text + end +end diff --git a/db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb b/db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb new file mode 100644 index 0000000000..b80808946d --- /dev/null +++ b/db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb @@ -0,0 +1,7 @@ +class MigrateShaAndRefToBuild < ActiveRecord::Migration + def change + execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL') + execute('UPDATE ci_builds SET push_data=(SELECT push_data FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE push_data IS NULL') + execute('UPDATE ci_builds SET tag=(SELECT tag FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE tag IS NULL') + end +end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index e625e790df..861da770d3 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -85,6 +85,7 @@ module Ci def build_job(name, job) { + stage_idx: stages.index(job[:stage]), stage: job[:stage], script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}", tags: job[:tags] || [], diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb index 3ab8c915df..f43a3982d7 100644 --- a/spec/requests/ci/commits_spec.rb +++ b/spec/requests/ci/commits_spec.rb @@ -7,7 +7,7 @@ describe "Commits" do describe "GET /:project/refs/:ref_name/commits/:id/status.json" do before do - get status_ci_project_ref_commits_path(@commit.project, @commit.ref, @commit.sha), format: :json + get status_ci_project_commits_path(@commit.project, @commit.sha), format: :json end it { expect(response.status).to eq(200) } From e3d870d7fc282a1f0a1028996c8b44e5d32b9cbf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 10:14:33 +0200 Subject: [PATCH 165/543] Add user to Ci::Build to have pusher email address --- app/models/ci/build.rb | 6 +-- app/models/ci/commit.rb | 4 +- .../project_services/gitlab_ci_service.rb | 2 +- app/models/user.rb | 1 + app/services/ci/create_builds_service.rb | 4 +- app/services/ci/create_commit_service.rb | 16 +------- ...=> 20151002121400_add_index_for_builds.rb} | 2 +- ...151002122929_add_ref_and_tag_to_builds.rb} | 3 +- ...002122943_migrate_ref_and_tag_to_build.rb} | 3 +- .../20151005075649_add_user_id_to_build.rb | 5 +++ lib/ci/api/commits.rb | 2 +- spec/factories/ci/builds.rb | 7 +++- spec/factories/ci/commits.rb | 40 ++----------------- 13 files changed, 29 insertions(+), 66 deletions(-) rename db/migrate/{20151002121400_add_index_for_build_name.rb => 20151002121400_add_index_for_builds.rb} (61%) rename db/migrate/{20151002122929_add_sha_and_ref_to_builds.rb => 20151002122929_add_ref_and_tag_to_builds.rb} (51%) rename db/migrate/{20151002122943_migrate_sha_and_ref_to_build.rb => 20151002122943_migrate_ref_and_tag_to_build.rb} (58%) create mode 100644 db/migrate/20151005075649_add_user_id_to_build.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bf3e891520..bfdc1c7486 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -32,9 +32,9 @@ module Ci belongs_to :commit, class_name: 'Ci::Commit' belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + belongs_to :user serialize :options - serialize :push_data validates :commit, presence: true validates :status, presence: true @@ -196,8 +196,8 @@ module Ci def project_recipients recipients = project.email_recipients.split(' ') - if project.email_add_pusher? && push_data[:user_email].present? - recipients << push_data[:user_email] + if project.email_add_pusher? && user.present? && user.notification_email.present? + recipients << user.notification_email end recipients.uniq diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 35134b6628..3c577e3f08 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -96,10 +96,10 @@ module Ci builds_without_retry.group(:stage_idx).select(:stage).last end - def create_builds(ref, tag, push_data, trigger_request = nil) + def create_builds(ref, tag, user, trigger_request = nil) return if skip_ci? && trigger_request.blank? return unless config_processor - CreateBuildsService.new.execute(self, config_processor, ref, tag, push_data, trigger_request) + CreateBuildsService.new.execute(self, config_processor, ref, tag, user, trigger_request) end def refs diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index fd10851653..8e2b395494 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -52,7 +52,7 @@ class GitlabCiService < CiService ci_project = Ci::Project.find_by(gitlab_id: project.id) if ci_project - Ci::CreateCommitService.new.execute(ci_project, data) + Ci::CreateCommitService.new.execute(ci_project, data, current_user) end end diff --git a/app/models/user.rb b/app/models/user.rb index 1069f8e366..c7e3992b6a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -130,6 +130,7 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy + has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build' # diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index e9c85410e5..77a4305071 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,6 +1,6 @@ module Ci class CreateBuildsService - def execute(commit, ref, tag, push_data, config_processor, trigger_request) + def execute(commit, ref, tag, user, config_processor, trigger_request) config_processor.stages.any? do |stage| builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) builds_attrs.map do |build_attrs| @@ -17,7 +17,7 @@ module Ci trigger_request: trigger_request, ref: ref, tag: tag, - push_data: push_data, + user: user, }) end end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index 9120a82edc..edbb07580c 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -1,6 +1,6 @@ module Ci class CreateCommitService - def execute(project, params) + def execute(project, params, user) before_sha = params[:before] sha = params[:checkout_sha] || params[:after] origin_ref = params[:ref] @@ -17,21 +17,9 @@ module Ci end tag = origin_ref.start_with?('refs/tags/') - push_data = { - before: before_sha, - after: sha, - ref: ref, - user_name: params[:user_name], - user_email: params[:user_email], - repository: params[:repository], - commits: params[:commits], - total_commits_count: params[:total_commits_count], - ci_yaml_file: params[:ci_yaml_file] - } - commit = project.gl_project.ensure_ci_commit(sha) commit.update_committed! - commit.create_builds(ref, tag, push_data) + commit.create_builds(ref, tag, user) commit end diff --git a/db/migrate/20151002121400_add_index_for_build_name.rb b/db/migrate/20151002121400_add_index_for_builds.rb similarity index 61% rename from db/migrate/20151002121400_add_index_for_build_name.rb rename to db/migrate/20151002121400_add_index_for_builds.rb index c6a81d7466..4ffc136391 100644 --- a/db/migrate/20151002121400_add_index_for_build_name.rb +++ b/db/migrate/20151002121400_add_index_for_builds.rb @@ -1,4 +1,4 @@ -class AddIndexForBuildName < ActiveRecord::Migration +class AddIndexForBuilds < ActiveRecord::Migration def up add_index :ci_builds, [:commit_id, :stage_idx, :created_at] end diff --git a/db/migrate/20151002122929_add_sha_and_ref_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb similarity index 51% rename from db/migrate/20151002122929_add_sha_and_ref_to_builds.rb rename to db/migrate/20151002122929_add_ref_and_tag_to_builds.rb index fc367341f1..e3d2ac1cea 100644 --- a/db/migrate/20151002122929_add_sha_and_ref_to_builds.rb +++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb @@ -1,7 +1,6 @@ -class AddShaAndRefToBuilds < ActiveRecord::Migration +class AddRefAndTagToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :tag, :boolean add_column :ci_builds, :ref, :string - add_column :ci_builds, :push_data, :text end end diff --git a/db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb similarity index 58% rename from db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb rename to db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb index b80808946d..01d7b3f677 100644 --- a/db/migrate/20151002122943_migrate_sha_and_ref_to_build.rb +++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb @@ -1,7 +1,6 @@ -class MigrateShaAndRefToBuild < ActiveRecord::Migration +class MigrateRefAndTagToBuild < ActiveRecord::Migration def change execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL') - execute('UPDATE ci_builds SET push_data=(SELECT push_data FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE push_data IS NULL') execute('UPDATE ci_builds SET tag=(SELECT tag FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE tag IS NULL') end end diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb new file mode 100644 index 0000000000..0f4b92b8b7 --- /dev/null +++ b/db/migrate/20151005075649_add_user_id_to_build.rb @@ -0,0 +1,5 @@ +class AddUserIdToBuild < ActiveRecord::Migration + def change + add_column :ci_builds, :user_id, :integer + end +end diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb index bac463a590..6a5b52b17d 100644 --- a/lib/ci/api/commits.rb +++ b/lib/ci/api/commits.rb @@ -51,7 +51,7 @@ module Ci required_attributes! [:project_id, :data, :project_token] project = Ci::Project.find(params[:project_id]) authenticate_project_token!(project) - commit = Ci::CreateCommitService.new.execute(project, params[:data]) + commit = Ci::CreateCommitService.new.execute(project, params[:data], current_user) if commit.persisted? present commit, with: Entities::CommitWithBuilds diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 99da5a1877..8e2496398a 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -27,9 +27,10 @@ FactoryGirl.define do factory :ci_build, class: Ci::Build do + ref 'master' + tag false started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' - commands 'ls -a' options do { image: "ruby:2.1", @@ -43,5 +44,9 @@ FactoryGirl.define do started_at nil finished_at nil end + + factory :ci_build_tag do + tag true + end end end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 9c7a0e9cbe..b74aae795a 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -18,59 +18,25 @@ # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do factory :ci_commit, class: Ci::Commit do - ref 'master' - before_sha '76de212e80737a608d939f648d959671fb0a0142' sha '97de212e80737a608d939f648d959671fb0a0142' - push_data do - { - ref: 'refs/heads/master', - before: '76de212e80737a608d939f648d959671fb0a0142', - after: '97de212e80737a608d939f648d959671fb0a0142', - user_name: 'Git User', - user_email: 'git@example.com', - repository: { - name: 'test-data', - url: 'ssh://git@gitlab.com/test/test-data.git', - description: '', - homepage: 'http://gitlab.com/test/test-data' - }, - commits: [ - { - id: '97de212e80737a608d939f648d959671fb0a0142', - message: 'Test commit message', - timestamp: '2014-09-23T13:12:25+02:00', - url: 'https://gitlab.com/test/test-data/commit/97de212e80737a608d939f648d959671fb0a0142', - author: { - name: 'Git User', - email: 'git@user.com' - } - } - ], - total_commits_count: 1, - ci_yaml_file: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - } - end gl_project factory: :empty_project factory :ci_commit_without_jobs do after(:create) do |commit, evaluator| - commit.push_data[:ci_yaml_file] = YAML.dump({}) - commit.save + allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } end end factory :ci_commit_with_one_job do after(:create) do |commit, evaluator| - commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" } }) - commit.save + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } end end factory :ci_commit_with_two_jobs do after(:create) do |commit, evaluator| - commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) - commit.save + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } end end end From c293cc91522544f5a45fc00b842fc23fa228cdf9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 5 Oct 2015 10:47:23 +0200 Subject: [PATCH 166/543] Move CI web hooks page to project settings area Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/web_hooks_controller.rb | 53 ------------------- .../projects/ci_web_hooks_controller.rb | 45 ++++++++++++++++ app/views/layouts/ci/_nav_project.html.haml | 5 -- .../layouts/nav/_project_settings.html.haml | 5 ++ .../ci_web_hooks}/index.html.haml | 8 +-- config/routes.rb | 11 ++-- spec/features/ci_web_hooks_spec.rb | 27 ++++++++++ 8 files changed, 87 insertions(+), 68 deletions(-) delete mode 100644 app/controllers/ci/web_hooks_controller.rb create mode 100644 app/controllers/projects/ci_web_hooks_controller.rb rename app/views/{ci/web_hooks => projects/ci_web_hooks}/index.html.haml (85%) create mode 100644 spec/features/ci_web_hooks_spec.rb diff --git a/CHANGELOG b/CHANGELOG index ec23d0f117..4a2dbdbaac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ v 8.1.0 (unreleased) - Ensure code blocks are properly highlighted after a note is updated - Fix wrong access level badge on MR comments - Hide password in the service settings form + - Move CI web hooks page to project settings area v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb deleted file mode 100644 index 24074a6d9a..0000000000 --- a/app/controllers/ci/web_hooks_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Ci - class WebHooksController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_access_project! - before_action :authorize_manage_project! - - layout 'ci/project' - - def index - @web_hooks = @project.web_hooks - @web_hook = Ci::WebHook.new - end - - def create - @web_hook = @project.web_hooks.new(web_hook_params) - @web_hook.save - - if @web_hook.valid? - redirect_to ci_project_web_hooks_path(@project) - else - @web_hooks = @project.web_hooks.select(&:persisted?) - render :index - end - end - - def test - Ci::TestHookService.new.execute(hook, current_user) - - redirect_to :back - end - - def destroy - hook.destroy - - redirect_to ci_project_web_hooks_path(@project) - end - - private - - def hook - @web_hook ||= @project.web_hooks.find(params[:id]) - end - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def web_hook_params - params.require(:web_hook).permit(:url) - end - end -end diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb new file mode 100644 index 0000000000..7f40ddcb3f --- /dev/null +++ b/app/controllers/projects/ci_web_hooks_controller.rb @@ -0,0 +1,45 @@ +class Projects::CiWebHooksController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout "project_settings" + + def index + @web_hooks = @ci_project.web_hooks + @web_hook = Ci::WebHook.new + end + + def create + @web_hook = @ci_project.web_hooks.new(web_hook_params) + @web_hook.save + + if @web_hook.valid? + redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) + else + @web_hooks = @ci_project.web_hooks.select(&:persisted?) + render :index + end + end + + def test + Ci::TestHookService.new.execute(hook, current_user) + + redirect_to :back + end + + def destroy + hook.destroy + + redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) + end + + private + + def hook + @web_hook ||= @ci_project.web_hooks.find(params[:id]) + end + + def web_hook_params + params.require(:web_hook).permit(:url) + end +end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 3a2741367c..545abc23d9 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -11,11 +11,6 @@ %span Commits %span.count= @project.commits.count - = nav_link path: 'web_hooks#index' do - = link_to ci_project_web_hooks_path(@project) do - = icon('link fw') - %span - Web Hooks = nav_link path: ['services#index', 'services#edit'] do = link_to ci_project_services_path(@project) do = icon('share fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 26cccb48f6..abee5e1b3f 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -50,6 +50,11 @@ = icon('retweet fw') %span Triggers + = nav_link path: 'web_hooks#index' do + = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do + = icon('link fw') + %span + CI Web Hooks = nav_link path: 'ci_settings#edit' do = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do = icon('building fw') diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml similarity index 85% rename from app/views/ci/web_hooks/index.html.haml rename to app/views/projects/ci_web_hooks/index.html.haml index 78e8203b25..c3fe4db814 100644 --- a/app/views/ci/web_hooks/index.html.haml +++ b/app/views/projects/ci_web_hooks/index.html.haml @@ -6,7 +6,7 @@ %hr.clearfix -= form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f| += form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| -if @web_hook.errors.any? .alert.alert-danger - @web_hook.errors.full_messages.each do |msg| @@ -28,9 +28,9 @@ %span.monospace= hook.url %td .pull-right - - if @project.commits.any? - = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped" - = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + - 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/config/routes.rb b/config/routes.rb index 6d96d8801c..7bda7ade81 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,12 +47,6 @@ Gitlab::Application.routes.draw do end end - resources :web_hooks, only: [:index, :create, :destroy] do - member do - get :test - end - end - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -591,6 +585,11 @@ Gitlab::Application.routes.draw do resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] resource :ci_settings, only: [:edit, :update, :destroy] + resources :ci_web_hooks, only: [:index, :create, :destroy] do + member do + get :test + end + end resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb new file mode 100644 index 0000000000..efae0a42c1 --- /dev/null +++ b/spec/features/ci_web_hooks_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'CI web hooks' do + let(:user) { create(:user) } + before { login_as(user) } + + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project) + end + + context 'create a trigger' do + before do + fill_in 'web_hook_url', with: 'http://example.com' + click_on 'Add Web Hook' + end + + it { expect(@project.web_hooks.count).to eq(1) } + + it 'revokes the trigger' do + click_on 'Remove' + expect(@project.web_hooks.count).to eq(0) + end + end +end From 22506ddc50146ab56b2f114d90ab7b3270d60de1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 5 Oct 2015 10:51:24 +0200 Subject: [PATCH 167/543] Added benchmark_subject method for benchmarks This class method can be used in "describe" blocks to specify the subject of a benchmark. This lets you write: benchmark_subject { Foo } instead of: benchmark_subject { -> { Foo } } --- spec/benchmarks/models/user_spec.rb | 8 ++++---- spec/support/matchers/benchmark_matchers.rb | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb index a08c84ffce..168be20b7a 100644 --- a/spec/benchmarks/models/user_spec.rb +++ b/spec/benchmarks/models/user_spec.rb @@ -14,25 +14,25 @@ describe User, benchmark: true do let(:iterations) { 1000 } describe 'using a capitalized username' do - subject { -> { User.by_login('Alice') } } + benchmark_subject { User.by_login('Alice') } it { is_expected.to iterate_per_second(iterations) } end describe 'using a lowercase username' do - subject { -> { User.by_login('alice') } } + benchmark_subject { User.by_login('alice') } it { is_expected.to iterate_per_second(iterations) } end describe 'using a capitalized Email address' do - subject { -> { User.by_login('Alice@gitlab.com') } } + benchmark_subject { User.by_login('Alice@gitlab.com') } it { is_expected.to iterate_per_second(iterations) } end describe 'using a lowercase Email address' do - subject { -> { User.by_login('alice@gitlab.com') } } + benchmark_subject { User.by_login('alice@gitlab.com') } it { is_expected.to iterate_per_second(iterations) } end diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb index 45a1d49345..b73a53917f 100644 --- a/spec/support/matchers/benchmark_matchers.rb +++ b/spec/support/matchers/benchmark_matchers.rb @@ -1,6 +1,10 @@ module BenchmarkMatchers extend RSpec::Matchers::DSL + def self.included(into) + into.extend(ClassMethods) + end + matcher :iterate_per_second do |min_iterations| supports_block_expectations @@ -39,4 +43,17 @@ module BenchmarkMatchers report.entries[0] end + + module ClassMethods + # Wraps around rspec's subject method so you can write: + # + # benchmark_subject { SomeClass.some_method } + # + # instead of: + # + # subject { -> { SomeClass.some_method } } + def benchmark_subject(&block) + subject { block } + end + end end From ecbe393b8d37f56f86c2695019230f0741d86553 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 5 Oct 2015 10:51:25 +0200 Subject: [PATCH 168/543] CI web hooks menu active state and consitent title Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_project_settings.html.haml | 2 +- app/views/projects/ci_web_hooks/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index abee5e1b3f..9279a84662 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -50,7 +50,7 @@ = icon('retweet fw') %span Triggers - = nav_link path: 'web_hooks#index' do + = nav_link path: 'ci_web_hooks#index' do = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do = icon('link fw') %span diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml index c3fe4db814..6aebd7cfc4 100644 --- a/app/views/projects/ci_web_hooks/index.html.haml +++ b/app/views/projects/ci_web_hooks/index.html.haml @@ -1,5 +1,5 @@ %h3.page-title - Web hooks + CI Web hooks %p.light Web Hooks can be used for binding events when build completed. From 89920ca8194d8bc946640fd786fb49ebd39e7f11 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 5 Oct 2015 11:08:52 +0200 Subject: [PATCH 169/543] Allow benchmark failures for the time being This will be disallowed again once the existing benchmarks pass (which relies on #2341). --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9aaf82ec1..cf6d28b01a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,6 +30,7 @@ spec:benchmark: tags: - ruby - mysql + allow_failure: true spec:other: script: From 0bef64911b983f4fc1c130e9c5c7b730adb30072 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 5 Oct 2015 11:32:22 +0200 Subject: [PATCH 170/543] Added documentation for writing benchmarks --- doc/development/README.md | 1 + doc/development/benchmarking.md | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 doc/development/benchmarking.md diff --git a/doc/development/README.md b/doc/development/README.md index 6bc8e1888d..d5bf166ad3 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -8,3 +8,4 @@ - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [How to dump production data to staging](dump_db.md) +- [Benchmarking](benchmarking.md) diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md new file mode 100644 index 0000000000..88e18ee95f --- /dev/null +++ b/doc/development/benchmarking.md @@ -0,0 +1,69 @@ +# Benchmarking + +GitLab CE comes with a set of benchmarks that are executed for every build. This +makes it easier to measure performance of certain components over time. + +Benchmarks are written as RSpec tests using a few extra helpers. To write a +benchmark, first tag the top-level `describe`: + +```ruby +describe MaruTheCat, benchmark: true do + +end +``` + +This ensures the benchmark is executed separately from other test collections. +It also exposes the various RSpec matchers used for writing benchmarks to the +test group. + +Next, lets write the actual benchmark: + +```ruby +describe MaruTheCat, benchmark: true do + let(:maru) { MaruTheChat.new } + + describe '#jump_in_box' do + benchmark_subject { maru.jump_in_box } + + it { is_expected.to iterate_per_second(9000) } + end +end +``` + +Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that +makes it easier to specify the subject of a benchmark. Using RSpec's regular +`subject` would require us to write the following instead: + +```ruby +subject { -> { maru.jump_in_box } } +``` + +The `iterate_per_second` matcher defines the amount of times per second a +subject should be executed. The higher the amount of iterations the better. + +By default the allowed standard deviation is a maximum of 30%. This can be +adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second` +matcher: + +```ruby +it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) } +``` + +This can be useful if the code in question depends on external resources of +which the performance can vary a lot (e.g. physical HDDs, network calls, etc). +However, in most cases 30% should be enough so only change this when really +needed. + +## Benchmarks Location + +Benchmarks should be stored in `spec/benchmarks` and should follow the regular +Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`, +benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc. + +## Underlying Technology + +The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the +heavy lifting such as warming up code, calculating iterations, standard +deviation, etc. + +[benchmark-ips]: https://github.com/evanphx/benchmark-ips From 317a7469545d0e9a70e54a87a540b8aabe4c418b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 12:02:26 +0200 Subject: [PATCH 171/543] Make commit_spec run --- app/models/ci/build.rb | 4 +- app/models/ci/commit.rb | 75 ++++++---- app/services/ci/create_builds_service.rb | 39 +++-- app/services/ci/web_hook_service.rb | 2 - app/views/ci/builds/show.html.haml | 5 - app/views/ci/commits/show.html.haml | 11 +- db/schema.rb | 101 ++++++++----- lib/ci/gitlab_ci_yaml_processor.rb | 4 +- spec/factories/ci/commits.rb | 16 ++- spec/models/ci/build_spec.rb | 51 +++++-- spec/models/ci/commit_spec.rb | 134 ++++++------------ .../mail_service_spec.rb | 0 spec/spec_helper.rb | 4 + 13 files changed, 232 insertions(+), 214 deletions(-) rename spec/models/ci/{ => project_services}/mail_service_spec.rb (100%) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bfdc1c7486..79f040b895 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -80,6 +80,8 @@ module Ci def retry(build) new_build = Ci::Build.new(status: :pending) + new_build.ref = build.ref + new_build.tag = build.tag new_build.options = build.options new_build.commands = build.commands new_build.tag_list = build.tag_list @@ -141,7 +143,7 @@ module Ci state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :before_sha, :ref, :project, + delegate :sha, :short_sha, :project, to: :commit, prefix: false def trace_html diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 3c577e3f08..59d4932d43 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -58,14 +58,6 @@ module Ci end end - def new_branch? - before_sha == Ci::Git::BLANK_SHA - end - - def compare? - !new_branch? - end - def git_author_name commit_data.author_name if commit_data end @@ -78,10 +70,6 @@ module Ci commit_data.message if commit_data end - def short_before_sha - Ci::Commit.truncate_sha(before_sha) - end - def short_sha Ci::Commit.truncate_sha(sha) end @@ -99,7 +87,22 @@ module Ci def create_builds(ref, tag, user, trigger_request = nil) return if skip_ci? && trigger_request.blank? return unless config_processor - CreateBuildsService.new.execute(self, config_processor, ref, tag, user, trigger_request) + config_processor.stages.any? do |stage| + CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? + end + end + + def create_next_builds(ref, tag, user, trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage) + + config_processor.stages.any? do |stage| + unless stages.include?(stage) + CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? + end + end end def refs @@ -111,7 +114,14 @@ module Ci end def builds_without_retry - builds.latest + @builds_without_retry ||= + begin + grouped_builds = builds.group_by(&:name) + latest_builds = grouped_builds.map do |name, builds| + builds.sort_by(&:id).last + end + latest_builds.sort_by(&:stage_idx) + end end def retried_builds @@ -125,32 +135,35 @@ module Ci return 'failed' elsif builds.none? return 'skipped' - end - - statuses = builds_without_retry.ignore_failures.pluck(:status) - if statuses.all? { |status| status == 'success' } - return 'success' - elsif statuses.all? { |status| status == 'pending' } - return 'pending' - elsif statuses.include?('running') || statuses.include?('pending') - return 'running' - elsif statuses.all? { |status| status == 'canceled' } - return 'canceled' + elsif success? + 'success' + elsif pending? + 'pending' + elsif running? + 'running' + elsif canceled? + 'canceled' else - return 'failed' + 'failed' end end def pending? - status == 'pending' + builds_without_retry.all? do |build| + build.pending? + end end def running? - status == 'running' + builds_without_retry.any? do |build| + build.running? || build.pending? + end end def success? - status == 'success' + builds_without_retry.all? do |build| + build.success? || build.ignored? + end end def failed? @@ -158,7 +171,9 @@ module Ci end def canceled? - status == 'canceled' + builds_without_retry.all? do |build| + build.canceled? + end end def duration diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 77a4305071..255fdc4110 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,26 +1,23 @@ module Ci class CreateBuildsService - def execute(commit, ref, tag, user, config_processor, trigger_request) - config_processor.stages.any? do |stage| - builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) - builds_attrs.map do |build_attrs| - # don't create the same build twice - unless commit.builds.find_by_name_and_trigger_request(name: build_attrs[:name], ref: ref, tag: tag, trigger_request: trigger_request) - commit.builds.create!({ - name: build_attrs[:name], - commands: build_attrs[:script], - tag_list: build_attrs[:tags], - options: build_attrs[:options], - allow_failure: build_attrs[:allow_failure], - stage: build_attrs[:stage], - stage_idx: build_attrs[:stage_idx], - trigger_request: trigger_request, - ref: ref, - tag: tag, - user: user, - }) - end - end + def execute(commit, stage, ref, tag, user, trigger_request) + builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag) + + builds_attrs.map do |build_attrs| + build_attrs.slice!(:name, + :commands, + :tag_list, + :options, + :allow_failure, + :stage, + :stage_idx) + + build_attrs.merge!(ref: ref, + tag: tag, + trigger_request: trigger_request, + user: user) + + commit.builds.create!(build_attrs) end end end diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb index 87984b20fa..4bbca5c7da 100644 --- a/app/services/ci/web_hook_service.rb +++ b/app/services/ci/web_hook_service.rb @@ -28,8 +28,6 @@ module Ci gitlab_url: project.gitlab_url, ref: build.ref, sha: build.sha, - before_sha: build.before_sha, - push_data: build.commit.push_data }) end end diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml index f941143e2e..e4ec190ada 100644 --- a/app/views/ci/builds/show.html.haml +++ b/app/views/ci/builds/show.html.haml @@ -122,11 +122,6 @@ Commit .pull-right %small #{build_commit_link @build} - - - if @build.commit.compare? - %p - %span.attr-name Compare: - #{build_compare_link @build} %p %span.attr-name Branch: #{build_ref_link @build} diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml index 4badb67128..6948732017 100644 --- a/app/views/ci/commits/show.html.haml +++ b/app/views/ci/commits/show.html.haml @@ -9,14 +9,9 @@ .gray-content-block.second-block .row .col-sm-6 - - if @commit.compare? - %p - %span.attr-name Compare: - #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)} - - else - %p - %span.attr-name Commit: - #{gitlab_commit_link(@project, @commit.sha)} + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} .col-sm-6 - if @commit.git_author_name || @commit.git_author_email %p diff --git a/db/schema.rb b/db/schema.rb index 72609da93f..f2bda91f79 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,10 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150930095736) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" +ActiveRecord::Schema.define(version: 20151005075649) do create_table "abuse_reports", force: true do |t| t.integer "reporter_id" @@ -45,8 +42,8 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" - t.text "help_page_text" t.boolean "ci_enabled", default: true, null: false + t.text "help_page_text" end create_table "audit_events", force: true do |t| @@ -85,37 +82,60 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.integer "project_id" t.string "status" t.datetime "finished_at" - t.text "trace" + t.text "trace", limit: 2147483647 t.datetime "created_at" t.datetime "updated_at" t.datetime "started_at" t.integer "runner_id" - t.float "coverage" + t.float "coverage", limit: 24 t.integer "commit_id" t.text "commands" t.integer "job_id" t.string "name" - t.boolean "deploy", default: false + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false + t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" + t.integer "gl_project_id" + t.integer "stage_idx" + t.boolean "tag" + t.string "ref" + t.text "push_data" + t.integer "user_id" end + add_index "ci_builds", ["commit_id", "name"], name: "index_ci_builds_on_commit_id_and_name", using: :btree + 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"], name: "index_ci_builds_on_commit_id", using: :btree 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 + create_table "ci_commit_statuses", force: true do |t| + t.integer "commit_id" + t.string "sha" + t.string "ref" + t.string "state" + t.string "target_url" + t.string "description" + t.string "context", default: "default" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_commit_statuses", ["commit_id", "context", "ref"], name: "index_ci_commit_statuses_on_commit_id_and_context_and_ref", using: :btree + add_index "ci_commit_statuses", ["commit_id", "ref"], name: "index_ci_commit_statuses_on_commit_id_and_ref", using: :btree + create_table "ci_commits", force: true do |t| t.integer "project_id" t.string "ref" t.string "sha" t.string "before_sha" - t.text "push_data" + t.text "push_data", limit: 16777215 t.datetime "created_at" t.datetime "updated_at" - t.boolean "tag", default: false + t.boolean "tag", default: false t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" @@ -134,6 +154,7 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.text "description" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree @@ -181,10 +202,11 @@ ActiveRecord::Schema.define(version: 20150930095736) do end create_table "ci_runner_projects", force: true do |t| - t.integer "runner_id", null: false - t.integer "project_id", null: false + t.integer "runner_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree @@ -208,11 +230,12 @@ ActiveRecord::Schema.define(version: 20150930095736) do create_table "ci_services", force: true do |t| t.string "type" t.string "title" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" + t.integer "gl_project_id" end add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree @@ -257,10 +280,11 @@ ActiveRecord::Schema.define(version: 20150930095736) do create_table "ci_triggers", force: true do |t| t.string "token" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree @@ -272,15 +296,17 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.text "encrypted_value" t.string "encrypted_value_salt" t.string "encrypted_value_iv" + t.integer "gl_project_id" end add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree create_table "ci_web_hooks", force: true do |t| - t.string "url", null: false - t.integer "project_id", null: false + t.string "url", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.integer "gl_project_id" end create_table "deploy_keys_projects", force: true do |t| @@ -426,9 +452,9 @@ ActiveRecord::Schema.define(version: 20150930095736) do create_table "merge_request_diffs", force: true do |t| t.string "state" - t.text "st_commits" - t.text "st_diffs" - t.integer "merge_request_id", null: false + t.text "st_commits", limit: 2147483647 + t.text "st_diffs", limit: 2147483647 + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -511,8 +537,8 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false - t.text "st_diff" + t.boolean "system", default: false, null: false + t.text "st_diff", limit: 2147483647 t.integer "updated_by_id" end @@ -581,25 +607,26 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", limit: 24, default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 + t.boolean "shared_runners_enabled", default: false end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -651,15 +678,15 @@ ActiveRecord::Schema.define(version: 20150930095736) do create_table "snippets", force: true do |t| t.string "title" - t.text "content" - t.integer "author_id", null: false + t.text "content", limit: 2147483647 + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" t.string "type" - t.integer "visibility_level", default: 0, null: false + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 861da770d3..c47951bc5d 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -87,8 +87,8 @@ module Ci { stage_idx: stages.index(job[:stage]), stage: job[:stage], - script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}", - tags: job[:tags] || [], + commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}", + tag_list: job[:tags] || [], name: name, only: job[:only], except: job[:except], diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index b74aae795a..9226e04a7b 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -23,20 +23,26 @@ FactoryGirl.define do gl_project factory: :empty_project factory :ci_commit_without_jobs do - after(:create) do |commit, evaluator| + after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } end end factory :ci_commit_with_one_job do - after(:create) do |commit, evaluator| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { YAML.dump({rspec: {script: "ls"}}) } end end factory :ci_commit_with_two_jobs do - after(:create) do |commit, evaluator| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { YAML.dump({rspec: {script: "ls"}, spinach: {script: "ls"}}) } + end + end + + factory :ci_commit_yaml_stub do + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index ca070a1497..d74063e578 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -28,11 +28,14 @@ require 'spec_helper' describe Ci::Build do 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 } + let(:commit) { FactoryGirl.create :ci_commit_yaml_stub, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } + subject { build } it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:user) } it { is_expected.to validate_presence_of :status } + it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } @@ -236,12 +239,6 @@ describe Ci::Build do it { is_expected.to eq(options) } end - describe :ref do - subject { build.ref } - - it { is_expected.to eq(commit.ref) } - end - describe :sha do subject { build.sha } @@ -254,12 +251,6 @@ describe Ci::Build do it { is_expected.to eq(commit.short_sha) } end - describe :before_sha do - subject { build.before_sha } - - it { is_expected.to eq(commit.before_sha) } - end - describe :allow_git_fetch do subject { build.allow_git_fetch } @@ -359,4 +350,38 @@ describe Ci::Build do end end end + + describe :project_recipients do + let (:pusher_email) { 'pusher@gitlab.test' } + let (:user) { User.new(notification_email: pusher_email) } + subject { build.project_recipients } + + before do + build.update_attributes(user: user) + end + + it 'should return pusher_email as only recipient when no additional recipients are given' do + project.update_attributes(email_add_pusher: true, + email_recipients: '') + is_expected.to eq([pusher_email]) + end + + it 'should return pusher_email and additional recipients' do + project.update_attributes(email_add_pusher: true, + email_recipients: 'rec1 rec2') + is_expected.to eq(['rec1', 'rec2', pusher_email]) + end + + it 'should return recipients' do + project.update_attributes(email_add_pusher: false, + email_recipients: 'rec1 rec2') + is_expected.to eq(['rec1', 'rec2']) + end + + it 'should return unique recipients only' do + project.update_attributes(email_add_pusher: true, + email_recipients: "rec1 rec1 #{pusher_email}") + is_expected.to eq(['rec1', pusher_email]) + end + end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index c04bbcbadc..63602e89e3 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -21,15 +21,10 @@ describe Ci::Commit do 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 } - let(:commit_with_project) { FactoryGirl.create :ci_commit, gl_project: gl_project } - let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) } it { is_expected.to belong_to(:gl_project) } it { is_expected.to have_many(:builds) } - it { is_expected.to validate_presence_of :before_sha } it { is_expected.to validate_presence_of :sha } - it { is_expected.to validate_presence_of :ref } - it { is_expected.to validate_presence_of :push_data } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } @@ -59,53 +54,6 @@ describe Ci::Commit do end end - describe :project_recipients do - - context 'always sending notification' do - it 'should return commit_pusher_email as only recipient when no additional recipients are given' do - project = FactoryGirl.create :ci_project, - email_add_pusher: true, - email_recipients: '' - gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, gl_project: gl_project - expected = 'commit_pusher_email' - allow(commit).to receive(:push_data) { { user_email: expected } } - expect(commit.project_recipients).to eq([expected]) - end - - it 'should return commit_pusher_email and additional recipients' do - project = FactoryGirl.create :ci_project, - email_add_pusher: true, - email_recipients: 'rec1 rec2' - gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, gl_project: gl_project - expected = 'commit_pusher_email' - allow(commit).to receive(:push_data) { { user_email: expected } } - expect(commit.project_recipients).to eq(['rec1', 'rec2', expected]) - end - - it 'should return recipients' do - project = FactoryGirl.create :ci_project, - email_add_pusher: false, - email_recipients: 'rec1 rec2' - gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, gl_project: gl_project - expect(commit.project_recipients).to eq(['rec1', 'rec2']) - end - - it 'should return unique recipients only' do - project = FactoryGirl.create :ci_project, - email_add_pusher: true, - email_recipients: 'rec1 rec1 rec2' - gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project - commit = FactoryGirl.create :ci_commit, gl_project: gl_project - expected = 'rec2' - allow(commit).to receive(:push_data) { { user_email: expected } } - expect(commit.project_recipients).to eq(['rec1', 'rec2']) - end - end - end - describe :valid_commit_sha do context 'commit.sha can not start with 00000000' do before do @@ -117,14 +65,6 @@ describe Ci::Commit do end end - describe :compare? do - subject { commit_with_project.compare? } - - context 'if commit.before_sha are not nil' do - it { is_expected.to be_truthy } - end - end - describe :short_sha do subject { commit.short_before_sha } @@ -144,36 +84,51 @@ describe Ci::Commit do end describe :create_next_builds do - before do - allow(commit).to receive(:config_processor).and_return(config_processor) - end - - it "creates builds for next type" do - expect(commit.create_builds).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) - - expect(commit.create_next_builds(nil)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(4) - - expect(commit.create_next_builds(nil)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(5) - - expect(commit.create_next_builds(nil)).to be_falsey - end end describe :create_builds do - before do - allow(commit).to receive(:config_processor).and_return(config_processor) + let(:commit) { FactoryGirl.create :ci_commit_yaml_stub, 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) end it 'creates builds' do - expect(commit.create_builds).to be_truthy + expect(create_builds).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(2) + + expect(create_next_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + + expect(create_next_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(5) + + expect(create_next_builds).to be_falsey + end + + context 'for different ref' do + def create_develop_builds + commit.create_builds('develop', false, nil, nil) + end + + it 'creates builds' do + expect(create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(create_develop_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + expect(commit.refs.size).to eq(2) + expect(commit.builds.pluck(:name).uniq.size).to eq(2) + end end context 'for build triggers' do @@ -181,40 +136,39 @@ describe Ci::Commit do let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger } it 'creates builds' do - expect(commit.create_builds(trigger_request)).to be_truthy + expect(create_builds(trigger_request)).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(2) end it 'rebuilds commit' do - expect(commit.create_builds).to be_truthy + expect(create_builds).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(2) - expect(commit.create_builds(trigger_request)).to be_truthy + expect(create_builds(trigger_request)).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(4) end it 'creates next builds' do - expect(commit.create_builds(trigger_request)).to be_truthy + expect(create_builds(trigger_request)).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(2) - expect(commit.create_next_builds(trigger_request)).to be_truthy + expect(create_next_builds(trigger_request)).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(4) end context 'for [ci skip]' do before do - commit.push_data[:commits][0][:message] = 'skip this commit [ci skip]' - commit.save + allow(commit).to receive(:git_commit_message) { 'message [ci skip]' } end it 'rebuilds commit' do expect(commit.status).to eq('skipped') - expect(commit.create_builds(trigger_request)).to be_truthy + expect(create_builds(trigger_request)).to be_truthy commit.builds.reload expect(commit.builds.size).to eq(2) expect(commit.status).to eq('pending') diff --git a/spec/models/ci/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb similarity index 100% rename from spec/models/ci/mail_service_spec.rb rename to spec/models/ci/project_services/mail_service_spec.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dfe855926c..67f31ef54f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -42,4 +42,8 @@ RSpec.configure do |config| end end +FactoryGirl::SyntaxRunner.class_eval do + include RSpec::Mocks::ExampleMethods +end + ActiveRecord::Migration.maintain_test_schema! From 361dc3641dd28c4ecefbda94f7a8dad299c349aa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 12:38:00 +0200 Subject: [PATCH 172/543] Fix builds_without_retry --- app/models/ci/build.rb | 2 +- app/models/ci/commit.rb | 17 +++++++---------- app/views/ci/builds/show.html.haml | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 79f040b895..30a8b5aa81 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -47,7 +47,7 @@ module Ci scope :failed, ->() { where(status: "failed") } scope :unstarted, ->() { where(runner_id: nil) } scope :running_or_pending, ->() { where(status:[:running, :pending]) } - scope :latest, ->() { group(:name).order(stage_idx: :asc, created_at: :desc) } + scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name)).order(stage_idx: :asc) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :for_ref, ->(ref) { where(ref: ref) } diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 59d4932d43..31da7e8f2b 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -114,14 +114,11 @@ module Ci end def builds_without_retry - @builds_without_retry ||= - begin - grouped_builds = builds.group_by(&:name) - latest_builds = grouped_builds.map do |name, builds| - builds.sort_by(&:id).last - end - latest_builds.sort_by(&:stage_idx) - end + builds.latest + end + + def builds_without_retry_for_ref(ref) + builds.for_ref(ref).latest end def retried_builds @@ -181,7 +178,7 @@ module Ci end def duration_for_ref(ref) - builds_without_retry.for_ref(ref).select(&:duration).sum(&:duration).to_i + builds_without_retry_for_ref(ref).select(&:duration).sum(&:duration).to_i end def finished_at @@ -198,7 +195,7 @@ module Ci end def matrix_for_ref?(ref) - builds_without_retry.for_ref(ref).pluck(:id).size > 1 + builds_without_retry_for_ref(ref).pluck(:id).size > 1 end def config_processor diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml index e4ec190ada..c42d11bf05 100644 --- a/app/views/ci/builds/show.html.haml +++ b/app/views/ci/builds/show.html.haml @@ -1,7 +1,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu - - @commit.builds_without_retry.for_ref(build.ref).each do |build| + - @commit.builds_without_retry_for_ref(build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to ci_project_build_url(@project, build) do = ci_icon_for_status(build.status) @@ -12,7 +12,7 @@ = build.id - - unless @commit.builds_without_retry.for_ref(@build.ref).include?(@build) + - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) %li.active %a Build ##{@build.id} From 31330e4a3c7d21e4a86b86fd0f199a857d777a3b Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 5 Oct 2015 13:12:26 +0300 Subject: [PATCH 173/543] Fix anchors to comments in diffs --- CHANGELOG | 1 + app/assets/javascripts/merge_request_tabs.js.coffee | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ec23d0f117..05af03b9b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ v 8.1.0 (unreleased) - Ensure code blocks are properly highlighted after a note is updated - Fix wrong access level badge on MR comments - Hide password in the service settings form + - Fix anchors to comments in diffs v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 19a07b6a03..4e56791bde 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -66,6 +66,11 @@ class @MergeRequestTabs @setCurrentAction(action) + scrollToElement: (container) -> + if window.location.hash + top = $(container + " " + window.location.hash).offset().top + $('body').scrollTo(top); + # Activate a tab based on the current action activateTab: (action) -> action = 'notes' if action == 'show' @@ -122,6 +127,7 @@ class @MergeRequestTabs document.getElementById('commits').innerHTML = data.html $('.js-timeago').timeago() @commitsLoaded = true + @scrollToElement(".commits") loadDiff: (source) -> return if @diffsLoaded @@ -131,6 +137,7 @@ class @MergeRequestTabs success: (data) => document.getElementById('diffs').innerHTML = data.html @diffsLoaded = true + @scrollToElement(".diffs") toggleLoading: -> $('.mr-loading-status .loading').toggle() From d2d2df0738f3cd8311963c34d90ebc8ce4081aa6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 13:12:00 +0200 Subject: [PATCH 174/543] Fix next round of tests --- app/controllers/ci/commits_controller.rb | 2 +- app/helpers/builds_helper.rb | 4 -- .../project_services/ci/hip_chat_message.rb | 9 +--- .../project_services/ci/slack_message.rb | 23 ++++----- .../project_services/gitlab_ci_service.rb | 2 +- app/services/ci/create_builds_service.rb | 27 ++++++----- app/services/ci/create_commit_service.rb | 3 +- app/views/ci/commits/show.html.haml | 6 +-- lib/ci/api/commits.rb | 2 +- spec/factories/ci/commits.rb | 4 +- spec/features/ci/commits_spec.rb | 5 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 12 ++--- spec/models/ci/build_spec.rb | 6 +-- spec/models/ci/commit_spec.rb | 11 +---- .../project_services/hip_chat_message_spec.rb | 6 +-- .../ci/project_services/mail_service_spec.rb | 13 ++--- .../ci/project_services/slack_message_spec.rb | 6 +-- spec/requests/ci/api/builds_spec.rb | 8 ++-- .../services/ci/create_commit_service_spec.rb | 47 +++++++++++++------ spec/support/stub_gitlab_calls.rb | 8 ++++ 20 files changed, 103 insertions(+), 101 deletions(-) diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb index acf9189572..887e92f84c 100644 --- a/app/controllers/ci/commits_controller.rb +++ b/app/controllers/ci/commits_controller.rb @@ -13,7 +13,7 @@ module Ci end def status - commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id], params[:ref_id]) + commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) rescue ActiveRecord::RecordNotFound render json: { status: "not_found" } diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index b6658e52d0..626f4e2f4c 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -3,10 +3,6 @@ module BuildsHelper gitlab_ref_link build.project, build.ref end - def build_compare_link build - gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha - end - def build_commit_link build gitlab_commit_link build.project, build.short_sha end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb index 73fdbe801c..0bf448d47f 100644 --- a/app/models/project_services/ci/hip_chat_message.rb +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -11,14 +11,7 @@ module Ci def to_s lines = Array.new lines.push("#{project.name} - ") - - if commit.matrix? - lines.push("Commit ##{commit.id}
") - else - first_build = commit.builds_without_retry.first - lines.push("Build '#{first_build.name}' ##{first_build.id}
") - end - + 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 7d254836fb..a89c01517b 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -23,15 +23,13 @@ module Ci def attachments fields = [] - if commit.matrix? - commit.builds_without_retry.each do |build| - next if build.allow_failure? - next unless build.failed? - fields << { - title: build.name, - value: "Build <#{ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." - } - end + commit.builds_without_retry.each do |build| + next if build.allow_failure? + next unless build.failed? + fields << { + title: build.name, + value: "Build <#{ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." + } end [{ @@ -47,12 +45,7 @@ module Ci def attachment_message out = "<#{ci_project_url(project)}|#{project_name}>: " - if commit.matrix? - out << "Commit <#{ci_project_commits_url(project, commit.sha)}|\##{commit.id}> " - else - build = commit.builds_without_retry.first - out << "Build <#{ci_project_build_url(project, build)}|\##{build.id}> " - end + out << "Commit <#{ci_project_commits_url(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 8e2b395494..17d1ef7194 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -52,7 +52,7 @@ class GitlabCiService < CiService ci_project = Ci::Project.find_by(gitlab_id: project.id) if ci_project - Ci::CreateCommitService.new.execute(ci_project, data, current_user) + Ci::CreateCommitService.new.execute(ci_project, current_user, data) end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 255fdc4110..c420f3268f 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -4,20 +4,23 @@ module Ci builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag) builds_attrs.map do |build_attrs| - build_attrs.slice!(:name, - :commands, - :tag_list, - :options, - :allow_failure, - :stage, - :stage_idx) + # don't create the same build twice + unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) + build_attrs.slice!(:name, + :commands, + :tag_list, + :options, + :allow_failure, + :stage, + :stage_idx) - build_attrs.merge!(ref: ref, - tag: tag, - trigger_request: trigger_request, - user: user) + build_attrs.merge!(ref: ref, + tag: tag, + trigger_request: trigger_request, + user: user) - commit.builds.create!(build_attrs) + commit.builds.create!(build_attrs) + end end end end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index edbb07580c..fc1ae5774d 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -1,7 +1,6 @@ module Ci class CreateCommitService - def execute(project, params, user) - before_sha = params[:before] + def execute(project, user, params) sha = params[:checkout_sha] || params[:after] origin_ref = params[:ref] diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml index 6948732017..7217671fe9 100644 --- a/app/views/ci/commits/show.html.haml +++ b/app/views/ci/commits/show.html.haml @@ -9,9 +9,9 @@ .gray-content-block.second-block .row .col-sm-6 - %p - %span.attr-name Commit: - #{gitlab_commit_link(@project, @commit.sha)} + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} .col-sm-6 - if @commit.git_author_name || @commit.git_author_email %p diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb index 6a5b52b17d..a60769d830 100644 --- a/lib/ci/api/commits.rb +++ b/lib/ci/api/commits.rb @@ -51,7 +51,7 @@ module Ci required_attributes! [:project_id, :data, :project_token] project = Ci::Project.find(params[:project_id]) authenticate_project_token!(project) - commit = Ci::CreateCommitService.new.execute(project, params[:data], current_user) + commit = Ci::CreateCommitService.new.execute(project, current_user, params[:data]) if commit.persisted? present commit, with: Entities::CommitWithBuilds diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 9226e04a7b..0c67e579a7 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -17,7 +17,7 @@ # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do - factory :ci_commit, class: Ci::Commit do + factory :ci_empty_commit, class: Ci::Commit do sha '97de212e80737a608d939f648d959671fb0a0142' gl_project factory: :empty_project @@ -40,7 +40,7 @@ FactoryGirl.define do end end - factory :ci_commit_yaml_stub do + factory :ci_commit do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } end diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb index 657a9dabe9..712a613726 100644 --- a/spec/features/ci/commits_spec.rb +++ b/spec/features/ci/commits_spec.rb @@ -38,10 +38,9 @@ describe "Commits" do end it "shows warning" do - @commit.push_data[:ci_yaml_file] = nil - @commit.save + @commit_no_yaml = FactoryGirl.create :ci_empty_commit - visit ci_commit_path(@commit) + visit ci_commit_path(@commit_no_yaml) expect(page).to have_content ".gitlab-ci.yml not found in this commit" end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 49482ac2b1..93568904d8 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -20,8 +20,8 @@ module Ci except: nil, name: :rspec, only: nil, - script: "pwd\nrspec", - tags: [], + commands: "pwd\nrspec", + tag_list: [], options: {}, allow_failure: false }) @@ -117,8 +117,8 @@ module Ci stage: "test", name: :rspec, only: nil, - script: "pwd\nrspec", - tags: [], + commands: "pwd\nrspec", + tag_list: [], options: { image: "ruby:2.1", services: ["mysql"] @@ -143,8 +143,8 @@ module Ci stage: "test", name: :rspec, only: nil, - script: "pwd\nrspec", - tags: [], + commands: "pwd\nrspec", + tag_list: [], options: { image: "ruby:2.5", services: ["postgresql"] diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d74063e578..da56f6e31a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' describe Ci::Build do let(:project) { FactoryGirl.create :ci_project } let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } - let(:commit) { FactoryGirl.create :ci_commit_yaml_stub, gl_project: gl_project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } subject { build } @@ -352,8 +352,8 @@ describe Ci::Build do end describe :project_recipients do - let (:pusher_email) { 'pusher@gitlab.test' } - let (:user) { User.new(notification_email: pusher_email) } + let(:pusher_email) { 'pusher@gitlab.test' } + let(:user) { User.new(notification_email: pusher_email) } subject { build.project_recipients } before do diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 63602e89e3..d18220f119 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -65,15 +65,6 @@ describe Ci::Commit do end end - describe :short_sha do - subject { commit.short_before_sha } - - it 'has 8 items' do - expect(subject.size).to eq(8) - end - it { expect(commit.before_sha).to start_with(subject) } - end - describe :short_sha do subject { commit.short_sha } @@ -87,7 +78,7 @@ describe Ci::Commit do end describe :create_builds do - let(:commit) { FactoryGirl.create :ci_commit_yaml_stub, 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) diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb index 1903c03692..6d25763835 100644 --- a/spec/models/ci/project_services/hip_chat_message_spec.rb +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -7,7 +7,7 @@ describe Ci::HipChatMessage do let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } let(:build) do - commit.create_builds + commit.create_builds('master', false, nil) commit.builds.first end @@ -43,7 +43,7 @@ describe Ci::HipChatMessage do context 'when all matrix builds succeed' do it 'returns a successful message' do - commit.create_builds + commit.create_builds('master', false, nil) commit.builds.update_all(status: "success") commit.reload @@ -56,7 +56,7 @@ describe Ci::HipChatMessage do context 'when at least one matrix build fails' do it 'returns a failure message' do - commit.create_builds + commit.create_builds('master', false, nil) first_build = commit.builds.first second_build = commit.builds.last first_build.update(status: "success") diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index 0d9f85959b..04e870dce7 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -29,12 +29,13 @@ describe Ci::MailService do describe 'Sends email for' do let(:mail) { Ci::MailService.new } + let(:user) { User.new(notification_email: 'git@example.com')} describe 'failed build' do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -57,7 +58,7 @@ describe Ci::MailService do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -85,7 +86,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -114,7 +115,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -143,7 +144,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -166,7 +167,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } before do allow(mail).to receive_messages( diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb index 7b541802d7..0870276c78 100644 --- a/spec/models/ci/project_services/slack_message_spec.rb +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -7,7 +7,7 @@ describe Ci::SlackMessage do let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } let(:build) do - commit.create_builds + commit.create_builds('master', false, nil) commit.builds.first end @@ -47,7 +47,7 @@ describe Ci::SlackMessage do let(:color) { 'good' } it 'returns a message with success' do - commit.create_builds + commit.create_builds('master', false, nil) commit.builds.update_all(status: "success") commit.reload @@ -63,7 +63,7 @@ describe Ci::SlackMessage do let(:color) { 'danger' } it 'returns a message with information about failed build' do - commit.create_builds + commit.create_builds('master', false, nil) first_build = commit.builds.first second_build = commit.builds.last first_build.update(status: "success") diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index bad250fbf4..576b0a11b9 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -19,7 +19,7 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) - commit.create_builds + commit.create_builds('master', false, nil) build = commit.builds.first post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -55,7 +55,7 @@ describe Ci::API::API do it "returns options" do commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) - commit.create_builds + commit.create_builds('master', false, nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -65,7 +65,7 @@ describe Ci::API::API do it "returns variables" do commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) - commit.create_builds + commit.create_builds('master', false, nil) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -82,7 +82,7 @@ describe Ci::API::API do commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) - commit.create_builds(trigger_request) + commit.create_builds('master', false, nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index 84ab0a615d..a797f73565 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -4,11 +4,16 @@ module Ci describe CreateCommitService do let(:service) { CreateCommitService.new } let(:project) { FactoryGirl.create(:ci_project) } + let(:user) { nil } + + before do + stub_ci_commit_to_return_yaml_file + end describe :execute do context 'valid params' do let(:commit) do - service.execute(project, + service.execute(project, user, ref: 'refs/heads/master', before: '00000000', after: '31das312', @@ -26,7 +31,7 @@ module Ci context "skip tag if there is no build for it" do it "creates commit if there is appropriate job" do - result = service.execute(project, + result = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', @@ -38,8 +43,9 @@ module Ci it "creates commit if there is no appropriate job but deploy job has right ref setting" do config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) + stub_ci_commit_yaml_file(config) - result = service.execute(project, + result = service.execute(project, user, ref: 'refs/heads/0_1', before: '00000000', after: '31das312', @@ -51,11 +57,12 @@ module Ci end it 'fails commits without .gitlab-ci.yml' do - result = service.execute(project, + stub_ci_commit_yaml_file(nil) + result = service.execute(project, user, ref: 'refs/heads/0_1', before: '00000000', after: '31das312', - ci_yaml_file: config, + ci_yaml_file: nil, commits: [ { message: 'Message' } ] ) expect(result).to be_persisted @@ -64,9 +71,15 @@ module Ci end describe :ci_skip? do + let (:message) { "some message[ci skip]" } + + before do + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + end + it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: "some message[ci skip]" }] - commit = service.execute(project, + commits = [{ message: message }] + commit = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', @@ -78,9 +91,10 @@ module Ci end it "does not skips builds creation if there is no [ci skip] tag in commit message" do - commits = [{ message: "some message" }] + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } - commit = service.execute(project, + commits = [{ message: "some message" }] + commit = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', @@ -92,8 +106,9 @@ module Ci end it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - commits = [{ message: "some message[ci skip]" }] - commit = service.execute(project, + stub_ci_commit_yaml_file('invalid: file') + commits = [{ message: message }] + commit = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', @@ -106,8 +121,10 @@ module Ci end it "skips build creation if there are already builds" do + allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } + commits = [{ message: "message" }] - commit = service.execute(project, + commit = service.execute(project, user, ref: 'refs/heads/master', before: '00000000', after: '31das312', @@ -116,7 +133,7 @@ module Ci ) expect(commit.builds.count(:all)).to eq(2) - commit = service.execute(project, + commit = service.execute(project, user, ref: 'refs/heads/master', before: '00000000', after: '31das312', @@ -127,9 +144,11 @@ module Ci end it "creates commit with failed status if yaml is invalid" do + stub_ci_commit_yaml_file('invalid: file') + commits = [{ message: "some message" }] - commit = service.execute(project, + commit = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 5e6744afda..5b3eb1bfc5 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -13,6 +13,14 @@ module StubGitlabCalls allow_any_instance_of(Network).to receive(:projects) { project_hash_array } end + def stub_ci_commit_to_return_yaml_file + stub_ci_commit_yaml_file(gitlab_ci_yaml) + end + + def stub_ci_commit_yaml_file(ci_yaml) + allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml } + end + private def gitlab_url From 782c8f9aa0a32def807da126e9f07f278772b6a2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 13:37:50 +0200 Subject: [PATCH 175/543] Fix triggers spec --- .../ci/create_trigger_request_service.rb | 10 ++++-- .../ci/create_trigger_request_service_spec.rb | 31 +++++-------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index f13ed787ed..3597372528 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,10 +1,14 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.gl_project.commit(ref) - return unless commit + target = project.gl_project.repository.rev_parse_target(ref) + return unless target - ci_commit = project.gl_project.ensure_ci_commit(commit.sha) + # check if ref is tag + sha = target.oid + tag = target.is_a?(Rugged::Tag) || target.is_a?(Rugged::Tag::Annotation) + + ci_commit = project.gl_project.ensure_ci_commit(sha) trigger_request = trigger.trigger_requests.create!( variables: variables ) diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 525a24cc20..7aa1912b2a 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -3,19 +3,19 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService do let(:service) { Ci::CreateTriggerRequestService.new } let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:gl_project) { FactoryGirl.create :project, gitlab_ci_project: project } let(:trigger) { FactoryGirl.create :ci_trigger, project: project } + before do + stub_ci_commit_to_return_yaml_file + end + describe :execute do context 'valid params' do subject { service.execute(project, trigger, 'master') } - before do - @commit = FactoryGirl.create :ci_commit, gl_project: gl_project - end - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject.commit).to eq(@commit) } + it { expect(subject.commit).to be_kind_of(Ci::Commit) } end context 'no commit for ref' do @@ -28,26 +28,11 @@ describe Ci::CreateTriggerRequestService do subject { service.execute(project, trigger, 'master') } before do - FactoryGirl.create :ci_commit_without_jobs, gl_project: gl_project + stub_ci_commit_yaml_file('{}') + FactoryGirl.create :ci_commit, gl_project: gl_project end it { expect(subject).to be_nil } end - - context 'for multiple commits' do - subject { service.execute(project, trigger, 'master') } - - before do - @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: gl_project - @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project - @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, gl_project: gl_project - end - - context 'retries latest one' do - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject).to be_persisted } - it { expect(subject.commit).to eq(@commit2) } - end - end end end From 5064c9038c1ae2fa6c48bc46c58f49c72ff1963a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 13:51:28 +0200 Subject: [PATCH 176/543] Fix next bunch of tests --- app/models/ci/build.rb | 4 ++++ .../ci/create_trigger_request_service.rb | 7 +++++-- spec/requests/ci/api/builds_spec.rb | 4 ++++ spec/requests/ci/api/triggers_spec.rb | 18 ++++++++++-------- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 30a8b5aa81..89af83d8ef 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -146,6 +146,10 @@ module Ci delegate :sha, :short_sha, :project, to: :commit, prefix: false + def before_sha + Gitlab::Git::BLANK_SHA + end + def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? html || '' diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 3597372528..083cea7720 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,10 +1,11 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - target = project.gl_project.repository.rev_parse_target(ref) - return unless target + return unless project.gl_project + return unless project.gl_project.repository # check if ref is tag + target = project.gl_project.repository.rev_parse_target(ref) sha = target.oid tag = target.is_a?(Rugged::Tag) || target.is_a?(Rugged::Tag::Annotation) @@ -16,6 +17,8 @@ module Ci if ci_commit.create_builds(ref, tag, nil, trigger_request) trigger_request end + rescue Rugged::OdbError + nil end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 576b0a11b9..54c1d0199f 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -7,6 +7,10 @@ describe Ci::API::API do let(:project) { FactoryGirl.create(:ci_project) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + before do + stub_ci_commit_to_return_yaml_file + end + describe "Builds API for runners" do let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") } diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index bbe98e7dac..c98a74dcc2 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -6,7 +6,7 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } let!(:project) { FactoryGirl.create(:ci_project) } - let!(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let!(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) } let!(:project2) { FactoryGirl.create(:ci_project) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do @@ -15,6 +15,10 @@ describe Ci::API::API do } end + before do + stub_ci_commit_to_return_yaml_file + end + context 'Handles errors' do it 'should return bad request if token is missing' do post ci_api("/projects/#{project.id}/refs/master/trigger") @@ -33,15 +37,13 @@ describe Ci::API::API do end context 'Have a commit' do - before do - @commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) - end + let(:commit) { project.commits.last } it 'should create builds' do post ci_api("/projects/#{project.id}/refs/master/trigger"), options expect(response.status).to eq(201) - @commit.builds.reload - expect(@commit.builds.size).to eq(2) + commit.builds.reload + expect(commit.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do @@ -70,8 +72,8 @@ describe Ci::API::API do it 'create trigger request with variables' do post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables) expect(response.status).to eq(201) - @commit.builds.reload - expect(@commit.builds.first.trigger_request.variables).to eq(variables) + commit.builds.reload + expect(commit.builds.first.trigger_request.variables).to eq(variables) end end end From 813c0ead2e969aa2c68e657f8b6a64ff0f0076d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 5 Oct 2015 13:59:01 +0200 Subject: [PATCH 177/543] Revert "Merge branch 'projects' into 'master' " This reverts commit 2b493695a39cd4e30e92cf7830e33f2f126cb30a, reversing changes made to b5c12f742ab7431257cc685477d3be16c8cc5f0e. --- app/assets/stylesheets/base/layout.scss | 22 --------------------- app/assets/stylesheets/generic/buttons.scss | 4 ---- app/assets/stylesheets/generic/sidebar.scss | 11 ++--------- app/assets/stylesheets/pages/projects.scss | 1 + app/views/layouts/_page.html.haml | 2 +- 5 files changed, 4 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index f0569a5e67..b91c15d891 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -1,34 +1,18 @@ html { overflow-y: scroll; - height: 100%; - margin: 0; &.touch .tooltip { display: none !important; } body { padding-top: $header-height; - height: 100%; - margin: 0; } } .container { padding-top: 0; - height: 100%; - width: 100%; z-index: 5; } -.content { - height: 100%; - width: 100%; -} - -.content section { - height: 100%; - display: table-row; -} - .container .content { margin: 0 0; } @@ -40,9 +24,3 @@ html { .container-limited { max-width: $fixed-layout-width; } - -.max-height { - height: 100%; - display: table; - width: 100%; -} \ No newline at end of file diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index a5fe589044..cf76f538e0 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -94,7 +94,6 @@ body { @mixin btn-info { @include border-radius(2px); - @include transition (all 0.2s ease 0s); border-width: 1px; border-style: solid; @@ -117,7 +116,6 @@ body { &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; border-style: solid; } @@ -125,7 +123,6 @@ body { @mixin btn-middle { @include border-radius(2px); - @include transition (all 0.2s ease 0s); border-width: 1px; border-style: solid; @@ -148,7 +145,6 @@ body { &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; border-style: solid; } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index d30fc6e189..c5ea3aca7c 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -1,7 +1,4 @@ .page-with-sidebar { - min-height: 100%; - height: 100%; - .sidebar-wrapper { position: fixed; top: 0; @@ -21,19 +18,15 @@ } .content-wrapper { - min-height: 900px; - display: table; + min-height: 100vh; width: 100%; padding: 20px; background: #EAEBEC; - height: 100%; - width: 100%; .container-fluid { background: #FFF; padding: $gl-padding; - height: 100%; - min-height: 100%; + min-height: 90vh; &.container-blank { background: none; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b7d046e891..818aa10aef 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -510,3 +510,4 @@ pre.light-well { .inline-form { display: inline-block; } + diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1f4ade81ed..2468687b56 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,5 +25,5 @@ = render "layouts/flash" %div{ class: container_class } .content - .clearfix.max-height + .clearfix = yield From 0367dbf04392a200b5a2e0fcbab6269ff283fa54 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 14:15:15 +0200 Subject: [PATCH 178/543] Fix build pipelining --- app/models/ci/build.rb | 5 ++-- app/models/ci/commit.rb | 10 +++++++ spec/models/ci/commit_spec.rb | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 89af83d8ef..f35224916e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -50,6 +50,7 @@ module Ci scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name)).order(stage_idx: :asc) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :for_ref, ->(ref) { where(ref: ref) } + scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } acts_as_taggable @@ -125,8 +126,8 @@ module Ci Ci::WebHookService.new.build_end(build) end - if build.commit.success? - build.commit.create_next_builds(build.trigger_request) + if build.commit.should_create_next_builds?(build) + build.commit.create_next_builds(build.ref, build.tag, build.user, build.trigger_request) end project.execute_services(build) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 31da7e8f2b..c77921979a 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -224,6 +224,16 @@ 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/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index d18220f119..91cf96a666 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -215,4 +215,59 @@ 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 f42078f7c14b3a8a32f486ab0e3ec7ef791fc097 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 14:31:51 +0200 Subject: [PATCH 179/543] Fix rest of tests --- .../ci/create_trigger_request_service.rb | 13 +-- app/services/ci/web_hook_service.rb | 1 + spec/factories/ci/builds.rb | 1 + spec/features/ci/commits_spec.rb | 8 +- .../project_services/hip_chat_message_spec.rb | 81 +++++----------- .../ci/project_services/slack_message_spec.rb | 93 ++++++------------- 6 files changed, 64 insertions(+), 133 deletions(-) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 083cea7720..ea82dbb2bf 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,15 +1,14 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - return unless project.gl_project - return unless project.gl_project.repository + commit = project.gl_project.commit(ref) + return unless commit # check if ref is tag - target = project.gl_project.repository.rev_parse_target(ref) - sha = target.oid - tag = target.is_a?(Rugged::Tag) || target.is_a?(Rugged::Tag::Annotation) + tag = project.gl_project.repository.find_tag(ref).present? + + ci_commit = project.gl_project.ensure_ci_commit(commit.sha) - ci_commit = project.gl_project.ensure_ci_commit(sha) trigger_request = trigger.trigger_requests.create!( variables: variables ) @@ -17,8 +16,6 @@ module Ci if ci_commit.create_builds(ref, tag, nil, trigger_request) trigger_request end - rescue Rugged::OdbError - nil end end end diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb index 4bbca5c7da..92e6df442b 100644 --- a/app/services/ci/web_hook_service.rb +++ b/app/services/ci/web_hook_service.rb @@ -27,6 +27,7 @@ module Ci project_name: project.name, gitlab_url: project.gitlab_url, ref: build.ref, + before_sha: build.before_sha, sha: build.sha, }) end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 8e2496398a..21b582afba 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -31,6 +31,7 @@ FactoryGirl.define do tag false started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' + commands 'ls -a' options do { image: "ruby:2.1", diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb index 712a613726..b4236e1e58 100644 --- a/spec/features/ci/commits_spec.rb +++ b/spec/features/ci/commits_spec.rb @@ -11,6 +11,10 @@ describe "Commits" do @commit.project.gl_project.team << [@user, :master] end + before do + stub_ci_commit_to_return_yaml_file + end + describe "GET /:project/commits/:sha" do before do visit ci_commit_path(@commit) @@ -38,9 +42,9 @@ describe "Commits" do end it "shows warning" do - @commit_no_yaml = FactoryGirl.create :ci_empty_commit + stub_ci_commit_yaml_file(nil) - visit ci_commit_path(@commit_no_yaml) + visit ci_commit_path(@commit) expect(page).to have_content ".gitlab-ci.yml not found in this commit" end diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb index 6d25763835..e23d6ae2c2 100644 --- a/spec/models/ci/project_services/hip_chat_message_spec.rb +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -3,70 +3,37 @@ require 'spec_helper' describe Ci::HipChatMessage do subject { Ci::HipChatMessage.new(build) } - context "One build" do - let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } - let(:build) do + let(:build) do + commit.builds.first + end + + context 'when all matrix builds succeed' do + it 'returns a successful message' do commit.create_builds('master', false, nil) - commit.builds.first - end + commit.builds.update_all(status: "success") + commit.reload - context 'when build succeeds' do - it 'returns a successful message' do - build.update(status: "success") - - expect( subject.status_color ).to eq 'green' - expect( subject.notify? ).to be_falsey - expect( subject.to_s ).to match(/Build '[^']+' #\d+/) - expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./) - end - end - - context 'when build fails' do - it 'returns a failure message' do - build.update(status: "failed") - - expect( subject.status_color ).to eq 'red' - expect( subject.notify? ).to be_truthy - expect( subject.to_s ).to match(/Build '[^']+' #\d+/) - expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./) - end + expect(subject.status_color).to eq 'green' + expect(subject.notify?).to be_falsey + expect(subject.to_s).to match(/Commit #\d+/) + expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./) end end - context "Several builds" do - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } + context 'when at least one matrix build fails' do + it 'returns a failure message' do + commit.create_builds('master', false, nil) + first_build = commit.builds.first + second_build = commit.builds.last + first_build.update(status: "success") + second_build.update(status: "failed") - let(:build) do - commit.builds.first - end - - context 'when all matrix builds succeed' do - it 'returns a successful message' do - commit.create_builds('master', false, nil) - commit.builds.update_all(status: "success") - commit.reload - - expect( subject.status_color ).to eq 'green' - expect( subject.notify? ).to be_falsey - expect( subject.to_s ).to match(/Commit #\d+/) - expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./) - end - end - - context 'when at least one matrix build fails' do - it 'returns a failure message' do - commit.create_builds('master', false, nil) - first_build = commit.builds.first - second_build = commit.builds.last - first_build.update(status: "success") - second_build.update(status: "failed") - - expect( subject.status_color ).to eq 'red' - expect( subject.notify? ).to be_truthy - expect( subject.to_s ).to match(/Commit #\d+/) - expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./) - end + expect(subject.status_color).to eq 'red' + expect(subject.notify?).to be_truthy + expect(subject.to_s).to match(/Commit #\d+/) + expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./) end end end diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb index 0870276c78..8adda6c86c 100644 --- a/spec/models/ci/project_services/slack_message_spec.rb +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -3,80 +3,41 @@ require 'spec_helper' describe Ci::SlackMessage do subject { Ci::SlackMessage.new(commit) } - context "One build" do - let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } - let(:build) do + context 'when all matrix builds succeeded' do + let(:color) { 'good' } + + it 'returns a message with success' do commit.create_builds('master', false, nil) - commit.builds.first - end + commit.builds.update_all(status: "success") + commit.reload - context 'when build succeeded' do - let(:color) { 'good' } - - it 'returns a message with succeeded build' do - build.update(status: "success") - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Build') - expect(subject.fallback).to include("\##{build.id}") - expect(subject.fallback).to include('succeeded') - expect(subject.attachments.first[:fields]).to be_empty - end - end - - context 'when build failed' do - let(:color) { 'danger' } - - it 'returns a message with failed build' do - build.update(status: "failed") - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Build') - expect(subject.fallback).to include("\##{build.id}") - expect(subject.fallback).to include('failed') - expect(subject.attachments.first[:fields]).to be_empty - end + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Commit') + expect(subject.fallback).to include("\##{commit.id}") + expect(subject.fallback).to include('succeeded') + expect(subject.attachments.first[:fields]).to be_empty end end - context "Several builds" do - let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } + context 'when one of matrix builds failed' do + let(:color) { 'danger' } - context 'when all matrix builds succeeded' do - let(:color) { 'good' } + it 'returns a message with information about failed build' do + commit.create_builds('master', false, nil) + first_build = commit.builds.first + second_build = commit.builds.last + first_build.update(status: "success") + second_build.update(status: "failed") - it 'returns a message with success' do - commit.create_builds('master', false, nil) - commit.builds.update_all(status: "success") - commit.reload - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Commit') - expect(subject.fallback).to include("\##{commit.id}") - expect(subject.fallback).to include('succeeded') - expect(subject.attachments.first[:fields]).to be_empty - end - end - - context 'when one of matrix builds failed' do - let(:color) { 'danger' } - - it 'returns a message with information about failed build' do - commit.create_builds('master', false, nil) - first_build = commit.builds.first - second_build = commit.builds.last - first_build.update(status: "success") - second_build.update(status: "failed") - - expect(subject.color).to eq(color) - expect(subject.fallback).to include('Commit') - expect(subject.fallback).to include("\##{commit.id}") - expect(subject.fallback).to include('failed') - expect(subject.attachments.first[:fields].size).to eq(1) - expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name) - expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}") - end + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Commit') + expect(subject.fallback).to include("\##{commit.id}") + expect(subject.fallback).to include('failed') + expect(subject.attachments.first[:fields].size).to eq(1) + expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name) + expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}") end end end From 198f4b703d98892e062525a37e2420429d83369d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 14:37:42 +0200 Subject: [PATCH 180/543] Fix db/schema.rb --- db/schema.rb | 94 ++++++++++++++++++++-------------------------------- 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index f2bda91f79..0e8d54fb26 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,6 +13,9 @@ ActiveRecord::Schema.define(version: 20151005075649) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "abuse_reports", force: true do |t| t.integer "reporter_id" t.integer "user_id" @@ -42,8 +45,8 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" - t.boolean "ci_enabled", default: true, null: false t.text "help_page_text" + t.boolean "ci_enabled", default: true, null: false end create_table "audit_events", force: true do |t| @@ -82,60 +85,42 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.integer "project_id" t.string "status" t.datetime "finished_at" - t.text "trace", limit: 2147483647 + t.text "trace" t.datetime "created_at" t.datetime "updated_at" t.datetime "started_at" t.integer "runner_id" - t.float "coverage", limit: 24 + t.float "coverage" t.integer "commit_id" t.text "commands" t.integer "job_id" t.string "name" - t.boolean "deploy", default: false + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false + t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" - t.integer "gl_project_id" t.integer "stage_idx" t.boolean "tag" t.string "ref" - t.text "push_data" t.integer "user_id" end - add_index "ci_builds", ["commit_id", "name"], name: "index_ci_builds_on_commit_id_and_name", using: :btree 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"], name: "index_ci_builds_on_commit_id", using: :btree 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 - create_table "ci_commit_statuses", force: true do |t| - t.integer "commit_id" - t.string "sha" - t.string "ref" - t.string "state" - t.string "target_url" - t.string "description" - t.string "context", default: "default" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "ci_commit_statuses", ["commit_id", "context", "ref"], name: "index_ci_commit_statuses_on_commit_id_and_context_and_ref", using: :btree - add_index "ci_commit_statuses", ["commit_id", "ref"], name: "index_ci_commit_statuses_on_commit_id_and_ref", using: :btree - create_table "ci_commits", force: true do |t| t.integer "project_id" t.string "ref" t.string "sha" t.string "before_sha" - t.text "push_data", limit: 16777215 + t.text "push_data" t.datetime "created_at" t.datetime "updated_at" - t.boolean "tag", default: false + t.boolean "tag", default: false t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" @@ -154,7 +139,6 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree @@ -202,11 +186,10 @@ ActiveRecord::Schema.define(version: 20151005075649) do end create_table "ci_runner_projects", force: true do |t| - t.integer "runner_id", null: false - t.integer "project_id", null: false + t.integer "runner_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree @@ -230,12 +213,11 @@ ActiveRecord::Schema.define(version: 20151005075649) do create_table "ci_services", force: true do |t| t.string "type" t.string "title" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.integer "gl_project_id" end add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree @@ -280,11 +262,10 @@ ActiveRecord::Schema.define(version: 20151005075649) do create_table "ci_triggers", force: true do |t| t.string "token" - t.integer "project_id", null: false + t.integer "project_id", null: false t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree @@ -296,17 +277,15 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.text "encrypted_value" t.string "encrypted_value_salt" t.string "encrypted_value_iv" - t.integer "gl_project_id" end add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree create_table "ci_web_hooks", force: true do |t| - t.string "url", null: false - t.integer "project_id", null: false + t.string "url", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id" end create_table "deploy_keys_projects", force: true do |t| @@ -452,9 +431,9 @@ ActiveRecord::Schema.define(version: 20151005075649) do create_table "merge_request_diffs", force: true do |t| t.string "state" - t.text "st_commits", limit: 2147483647 - t.text "st_diffs", limit: 2147483647 - t.integer "merge_request_id", null: false + t.text "st_commits" + t.text "st_diffs" + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -537,8 +516,8 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false - t.text "st_diff", limit: 2147483647 + t.boolean "system", default: false, null: false + t.text "st_diff" t.integer "updated_by_id" end @@ -607,26 +586,25 @@ ActiveRecord::Schema.define(version: 20151005075649) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", limit: 24, default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 - t.boolean "shared_runners_enabled", default: false + t.integer "commit_count", default: 0 end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -678,15 +656,15 @@ ActiveRecord::Schema.define(version: 20151005075649) do create_table "snippets", force: true do |t| t.string "title" - t.text "content", limit: 2147483647 - t.integer "author_id", null: false + t.text "content" + t.integer "author_id", null: false t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.string "file_name" t.datetime "expires_at" t.string "type" - t.integer "visibility_level", default: 0, null: false + t.integer "visibility_level", default: 0, null: false end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree From fb12b81b422e0d17751f4b63462baa503996cd37 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 14:39:56 +0200 Subject: [PATCH 181/543] Make rubocop happy --- spec/factories/ci/commits.rb | 4 ++-- spec/services/ci/create_commit_service_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 0c67e579a7..79e000b7cc 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -30,13 +30,13 @@ FactoryGirl.define do factory :ci_commit_with_one_job do after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({rspec: {script: "ls"}}) } + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } end end factory :ci_commit_with_two_jobs do after(:build) do |commit| - allow(commit).to receive(:ci_yaml_file) { YAML.dump({rspec: {script: "ls"}, spinach: {script: "ls"}}) } + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } end end diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index a797f73565..707fe779dc 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -71,7 +71,7 @@ module Ci end describe :ci_skip? do - let (:message) { "some message[ci skip]" } + let(:message) { "some message[ci skip]" } before do allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } From c9853897229ca5585c69c4675cbeefd9ca53147d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 15:59:31 +0200 Subject: [PATCH 182/543] Add stage tests --- app/models/ci/commit.rb | 3 ++- spec/models/ci/commit_spec.rb | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index c77921979a..46370034f9 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -81,7 +81,8 @@ module Ci end def stage - builds_without_retry.group(:stage_idx).select(:stage).last + running_or_pending = builds_without_retry.running_or_pending + running_or_pending.limit(1).pluck(:stage).first end def create_builds(ref, tag, user, trigger_request = nil) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 91cf96a666..acff1ddf0f 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -74,6 +74,40 @@ describe Ci::Commit do it { expect(commit.sha).to start_with(subject) } end + describe :stage do + subject { commit.stage } + + before do + @second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending + @first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending + end + + it 'returns first running stage' do + is_expected.to eq('test') + end + + context 'first build succeeded' do + before do + @first.update_attributes(status: :success) + end + + it 'returns last running stage' do + is_expected.to eq('deploy') + end + end + + context 'all builds succeeded' do + before do + @first.update_attributes(status: :success) + @second.update_attributes(status: :success) + end + + it 'returns nil' do + is_expected.to be_nil + end + end + end + describe :create_next_builds do end From a5f6af007a307b91b8fa45dfe821d5da31e32c47 Mon Sep 17 00:00:00 2001 From: Jan Bruckner Date: Mon, 5 Oct 2015 15:27:01 +0200 Subject: [PATCH 183/543] don't set LATEST_TAG automatically in patch update guide --- doc/update/patch_versions.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index a66a863f6c..da719229ab 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -23,9 +23,11 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- Gemfile.lock db/schema.rb -LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) -sudo -u git -H git checkout $LATEST_TAG -b $LATEST_TAG +sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` +Replace `LATEST_TAG` with the latest GitLab tag you want to update to, for example `v8.0.3`. +Use `git tag -l 'v*.[0-9]' --sort='v:refname'` to see a list of all tags. +Make sure to update patch versions only (check your current version with `cat VERSION`) ### 3. Update gitlab-shell to the corresponding version From 29a7c6796e75d3a36c613622c7c7507646fd93a0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 16:06:35 +0200 Subject: [PATCH 184/543] Fix GitLabCiService and remove ci_yaml_file from CI push data --- .../project_services/gitlab_ci_service.rb | 23 ++----------------- .../gitlab_ci_service_spec.rb | 8 +++---- spec/requests/ci/api/commits_spec.rb | 3 +-- .../services/ci/create_commit_service_spec.rb | 22 +++++------------- 4 files changed, 13 insertions(+), 43 deletions(-) diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 17d1ef7194..b63a75cf3a 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -40,18 +40,9 @@ class GitlabCiService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) - sha = data[:checkout_sha] - - if sha.present? - file = ci_yaml_file(sha) - - if file && file.data - data.merge!(ci_yaml_file: file.data) - end - end - - ci_project = Ci::Project.find_by(gitlab_id: project.id) + ci_project = project.gitlab_ci_project if ci_project + current_user = User.find_by(id: data[:user_id]) Ci::CreateCommitService.new.execute(ci_project, current_user, data) end end @@ -99,14 +90,4 @@ class GitlabCiService < CiService def fields [] end - - private - - def ci_yaml_file(sha) - repository.blob_at(sha, '.gitlab-ci.yml') - end - - def repository - project.repository - 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 683a403a90..c0b8a144c3 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -39,8 +39,8 @@ describe GitlabCiService do end describe :build_page do - it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/refs/master/commits/2ab7834c")} - it { expect(@service.build_page("issue#2", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/refs/master/commits/issue%232")} + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/commits/2ab7834c")} + it { expect(@service.build_page("issue#2", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/commits/issue%232")} end describe "execute" do @@ -48,8 +48,8 @@ describe GitlabCiService do let(:project) { create(:project, name: 'project') } let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } - it "calls ci_yaml_file" do - expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha]) + it "calls CreateCommitService" do + expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data) @service.execute(push_sample_data) end diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb index a41c321a30..6049135fd1 100644 --- a/spec/requests/ci/api/commits_spec.rb +++ b/spec/requests/ci/api/commits_spec.rb @@ -44,8 +44,7 @@ describe Ci::API::API, 'Commits' do "email" => "jordi@softcatala.org", } } - ], - ci_yaml_file: gitlab_ci_yaml + ] } end diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index 707fe779dc..e3a8fe9681 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -17,7 +17,6 @@ module Ci ref: 'refs/heads/master', before: '00000000', after: '31das312', - ci_yaml_file: gitlab_ci_yaml, commits: [ { message: "Message" } ] ) end @@ -35,7 +34,6 @@ module Ci ref: 'refs/tags/0_1', before: '00000000', after: '31das312', - ci_yaml_file: gitlab_ci_yaml, commits: [ { message: "Message" } ] ) expect(result).to be_persisted @@ -49,7 +47,6 @@ module Ci ref: 'refs/heads/0_1', before: '00000000', after: '31das312', - ci_yaml_file: config, commits: [ { message: "Message" } ] ) expect(result).to be_persisted @@ -62,7 +59,6 @@ module Ci ref: 'refs/heads/0_1', before: '00000000', after: '31das312', - ci_yaml_file: nil, commits: [ { message: 'Message' } ] ) expect(result).to be_persisted @@ -83,8 +79,7 @@ module Ci ref: 'refs/tags/0_1', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: gitlab_ci_yaml + commits: commits ) expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") @@ -98,8 +93,7 @@ module Ci ref: 'refs/tags/0_1', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: gitlab_ci_yaml + commits: commits ) expect(commit.builds.first.name).to eq("staging") @@ -112,8 +106,7 @@ module Ci ref: 'refs/tags/0_1', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: "invalid: file" + commits: commits ) expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") @@ -128,8 +121,7 @@ module Ci ref: 'refs/heads/master', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: gitlab_ci_yaml + commits: commits ) expect(commit.builds.count(:all)).to eq(2) @@ -137,8 +129,7 @@ module Ci ref: 'refs/heads/master', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: gitlab_ci_yaml + commits: commits ) expect(commit.builds.count(:all)).to eq(2) end @@ -152,8 +143,7 @@ module Ci ref: 'refs/tags/0_1', before: '00000000', after: '31das312', - commits: commits, - ci_yaml_file: "invalid: file" + commits: commits ) expect(commit.status).to eq("failed") From 517815f40f8c88e543279e259d0b983e70c5a49e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 16:07:50 +0200 Subject: [PATCH 185/543] Fix gitlab_ci_yaml_processor specs --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 93568904d8..aba957da48 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -17,6 +17,7 @@ module Ci 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({ stage: "test", + stage_idx: 1, except: nil, name: :rspec, only: nil, @@ -115,6 +116,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ except: nil, stage: "test", + stage_idx: 1, name: :rspec, only: nil, commands: "pwd\nrspec", @@ -141,6 +143,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ except: nil, stage: "test", + stage_idx: 1, name: :rspec, only: nil, commands: "pwd\nrspec", From 2fa4e2fb6a669b98ed6abc568eabc8f5f9f28e8a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 5 Oct 2015 16:26:37 +0200 Subject: [PATCH 186/543] Evaluate benchmark blocks in the proper context This ensures that blocks defines using "benchmark_subject" have access to methods defined using let/subject & friends. --- spec/support/matchers/benchmark_matchers.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb index b73a53917f..84f655c211 100644 --- a/spec/support/matchers/benchmark_matchers.rb +++ b/spec/support/matchers/benchmark_matchers.rb @@ -38,7 +38,9 @@ module BenchmarkMatchers # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry. def benchmark(&block) report = Benchmark.ips(quiet: true) do |bench| - bench.report(&block) + bench.report do + instance_eval(&block) + end end report.entries[0] From 144eef38507a5b3e21f8aa5e38247bdfd73fba23 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 5 Oct 2015 16:39:58 +0200 Subject: [PATCH 187/543] Update ci to ce doc to make it clearer when you are done if you don't want to keep your data. --- doc/migrate_ci_to_ce/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 1cb1bc2e76..5ec0a2069b 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -28,13 +28,15 @@ upgrade to 8.0 until you finish the migration procedure. ### Before upgrading -If you have GitLab CI installed using omnibus-gitlab packages but *you don't want to migrate your existing data*: +If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**: ```bash mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s) ``` -and run `sudo gitlab-ctl reconfigure`. +run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci`. + +If you want to migrate your existing data, continue reading. #### 0. Updating Omnibus from versions prior to 7.13 From 97a11136d3e7d7ed57c7571d296d7b50617dce16 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 16:21:18 +0200 Subject: [PATCH 188/543] Fix create_trigger_request_service_spec --- app/services/ci/create_trigger_request_service.rb | 3 ++- spec/requests/ci/api/triggers_spec.rb | 4 ++-- spec/services/ci/create_trigger_request_service_spec.rb | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index ea82dbb2bf..4b86cb0a1f 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -10,7 +10,8 @@ module Ci ci_commit = project.gl_project.ensure_ci_commit(commit.sha) trigger_request = trigger.trigger_requests.create!( - variables: variables + variables: variables, + commit: ci_commit, ) if ci_commit.create_builds(ref, tag, nil, trigger_request) diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index c98a74dcc2..93617fc4b3 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -5,8 +5,8 @@ describe Ci::API::API do describe 'POST /projects/:project_id/refs/:ref/trigger' do let!(:trigger_token) { 'secure token' } - let!(:project) { FactoryGirl.create(:ci_project) } - let!(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) } + let!(:gl_project) { FactoryGirl.create(:project) } + let!(:project) { FactoryGirl.create(:ci_project, gl_project: gl_project) } let!(:project2) { FactoryGirl.create(:ci_project) } let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } let(:options) do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 7aa1912b2a..fcafae3864 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService do let(:service) { Ci::CreateTriggerRequestService.new } - let(:project) { FactoryGirl.create :ci_project } - let(:gl_project) { FactoryGirl.create :project, gitlab_ci_project: project } - let(:trigger) { FactoryGirl.create :ci_trigger, project: project } + let(:gl_project) { create(:project) } + let(:project) { create(:ci_project, gl_project: gl_project) } + let(:trigger) { create(:ci_trigger, project: project) } before do stub_ci_commit_to_return_yaml_file @@ -15,7 +15,7 @@ describe Ci::CreateTriggerRequestService do subject { service.execute(project, trigger, 'master') } it { expect(subject).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject.commit).to be_kind_of(Ci::Commit) } + it { expect(subject.builds.first).to be_kind_of(Ci::Build) } end context 'no commit for ref' do From e2c5d08e7e4482bd13b8e2e4f77db73dc757d6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20G=C3=B6bel?= Date: Mon, 5 Oct 2015 17:22:47 +0200 Subject: [PATCH 189/543] added user preference to change layout width --- CHANGELOG | 1 + app/controllers/profiles/preferences_controller.rb | 1 + app/helpers/page_layout_helper.rb | 2 +- app/helpers/preferences_helper.rb | 7 +++++++ app/models/user.rb | 4 ++++ app/views/profiles/preferences/show.html.haml | 7 +++++++ app/views/profiles/preferences/update.js.erb | 7 +++++++ db/migrate/20151005150751_add_layout_option_for_users.rb | 5 +++++ db/schema.rb | 3 ++- 9 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20151005150751_add_layout_option_for_users.rb diff --git a/CHANGELOG b/CHANGELOG index a455650bed..58f1bab2d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 8.1.0 (unreleased) - Hide password in the service settings form - Fix anchors to comments in diffs - Move CI web hooks page to project settings area + - Add user preference to change layout width (Peter Göbel) v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index f83b4abd1e..a9a06ecc80 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -31,6 +31,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController def preferences_params params.require(:user).permit( :color_scheme_id, + :layout, :dashboard, :project_view, :theme_id diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index df37be51ce..8160253e59 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -26,7 +26,7 @@ module PageLayoutHelper def fluid_layout(enabled = false) if @fluid_layout.nil? - @fluid_layout = enabled + @fluid_layout = (current_user && current_user.layout == "wide") || enabled else @fluid_layout end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 1b1f4162df..8470d73349 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -1,5 +1,12 @@ # Helper methods for per-User preferences module PreferencesHelper + def layout_choices + [ + ['Small', :small], + ['Wide', :wide] + ] + end + # Maps `dashboard` values to more user-friendly option text DASHBOARD_CHOICES = { projects: 'Your Projects (default)', diff --git a/app/models/user.rb b/app/models/user.rb index 1069f8e366..112a85e784 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,6 +54,7 @@ # public_email :string(255) default(""), not null # dashboard :integer default(0) # project_view :integer default(0) +# layout :integer default(0) # require 'carrierwave/orm/activerecord' @@ -171,6 +172,9 @@ class User < ActiveRecord::Base after_create :post_create_hook after_destroy :post_destroy_hook + # User's Layout preference + enum layout: [:small, :wide] + # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 60289bfe7c..c12a358651 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -32,6 +32,13 @@ .panel-heading Behavior .panel-body + .form-group + = f.label :layout, class: 'control-label' do + Layout width + .col-sm-10 + = f.select :layout, layout_choices, {}, class: 'form-control' + .help-block + Choose between small (max. 1200px) and wide (100%) application layout .form-group = f.label :dashboard, class: 'control-label' do Default Dashboard diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 6c4b0ce757..97d856fa88 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -2,6 +2,13 @@ $('body').removeClass('<%= Gitlab::Themes.body_classes %>') $('body').addClass('<%= user_application_theme %>') +// Toggle container-fluid class +if ('<%= current_user.layout %>' === 'wide') { + $('.content-wrapper').find('.container-fluid').removeClass('container-limited') +} else { + $('.content-wrapper').find('.container-fluid').addClass('container-limited') +} + // Re-enable the "Save" button $('input[type=submit]').enable() diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb new file mode 100644 index 0000000000..5ead0c04d5 --- /dev/null +++ b/db/migrate/20151005150751_add_layout_option_for_users.rb @@ -0,0 +1,5 @@ +class AddLayoutOptionForUsers < ActiveRecord::Migration + def change + add_column :users, :layout, :integer, :default => 0 + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 72609da93f..186dfec886 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: 20151005150751) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -753,6 +753,7 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.integer "dashboard", default: 0 t.integer "project_view", default: 0 t.integer "consumed_timestep" + t.integer "layout", default: 0 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree From 8a78c4ea06c7bd3a4c6a0ea9943891327b90cd35 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 5 Oct 2015 18:01:28 +0200 Subject: [PATCH 190/543] Exclude benchmarks from the spec Rake task --- lib/tasks/spec.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 3ae5c25069..365ff2defd 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -42,7 +42,7 @@ desc "GitLab | Run specs" task :spec do cmds = [ %W(rake gitlab:setup), - %W(rspec spec), + %W(rspec spec --tag ~@benchmark), ] run_commands(cmds) end From 291e1fa93063a62e7a0f054135dc5bcebcdd0056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20G=C3=B6bel?= Date: Mon, 5 Oct 2015 18:09:05 +0200 Subject: [PATCH 191/543] improved code style and layout option naming --- app/helpers/page_layout_helper.rb | 2 +- app/helpers/preferences_helper.rb | 4 ++-- app/models/user.rb | 2 +- app/views/profiles/preferences/show.html.haml | 2 +- app/views/profiles/preferences/update.js.erb | 2 +- db/migrate/20151005150751_add_layout_option_for_users.rb | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 8160253e59..775cf5a3dd 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -26,7 +26,7 @@ module PageLayoutHelper def fluid_layout(enabled = false) if @fluid_layout.nil? - @fluid_layout = (current_user && current_user.layout == "wide") || enabled + @fluid_layout = (current_user && current_user.layout == "fluid") || enabled else @fluid_layout end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 8470d73349..4710171eba 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -2,8 +2,8 @@ module PreferencesHelper def layout_choices [ - ['Small', :small], - ['Wide', :wide] + ['Fixed', :fixed], + ['Fluid', :fluid] ] end diff --git a/app/models/user.rb b/app/models/user.rb index 112a85e784..8e44e828b8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -173,7 +173,7 @@ class User < ActiveRecord::Base after_destroy :post_destroy_hook # User's Layout preference - enum layout: [:small, :wide] + enum layout: [:fixed, :fluid] # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index c12a358651..01e285a8df 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 small (max. 1200px) and wide (100%) application layout + Choose between fixed (max. 1200px) and fluid (100%) application layout .form-group = f.label :dashboard, class: 'control-label' do Default Dashboard diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 97d856fa88..4433cab778 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -3,7 +3,7 @@ $('body').removeClass('<%= Gitlab::Themes.body_classes %>') $('body').addClass('<%= user_application_theme %>') // Toggle container-fluid class -if ('<%= current_user.layout %>' === 'wide') { +if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper').find('.container-fluid').removeClass('container-limited') } else { $('.content-wrapper').find('.container-fluid').addClass('container-limited') diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb index 5ead0c04d5..ead9b1f897 100644 --- a/db/migrate/20151005150751_add_layout_option_for_users.rb +++ b/db/migrate/20151005150751_add_layout_option_for_users.rb @@ -1,5 +1,5 @@ class AddLayoutOptionForUsers < ActiveRecord::Migration def change - add_column :users, :layout, :integer, :default => 0 + add_column :users, :layout, :integer, default: 0 end end \ No newline at end of file From 1bd4604ae5b77900a3fa5b9c388e5aeacf05af5d Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 5 Oct 2015 11:15:52 -0500 Subject: [PATCH 192/543] Added CHANGELOG item about fixed user identities API. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index a455650bed..a63aea7ca2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 8.1.0 (unreleased) - Hide password in the service settings form - Fix anchors to comments in diffs - 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. v 8.0.3 - Fix URL shown in Slack notifications From fc59c45d58124c4fc23d62ad2ed65cbfd6cb3563 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 12:23:38 -0400 Subject: [PATCH 193/543] Remove the option to disable CI This option only existed to ease the CI-to-CE/EE migration process. This commit partially reverts 8b05abe816b0c681ac218096b294311dd04fde8b --- app/controllers/admin/application_settings_controller.rb | 1 - app/controllers/ci/application_controller.rb | 9 --------- app/controllers/ci/projects_controller.rb | 8 ++------ app/models/application_setting.rb | 3 +-- app/views/admin/application_settings/_form.html.haml | 9 --------- app/views/ci/projects/disabled.html.haml | 1 - config/initializers/1_settings.rb | 1 - ...162154_remove_ci_enabled_from_application_settings.rb | 5 +++++ db/schema.rb | 3 +-- lib/ci/api/api.rb | 4 ---- lib/ci/api/helpers.rb | 6 ------ 11 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 app/views/ci/projects/disabled.html.haml create mode 100644 db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 5f70582cbb..7c134d2ec9 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -56,7 +56,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :restricted_signup_domains_raw, :version_check_enabled, :user_oauth_applications, - :ci_enabled, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index d8227e632e..9be470660e 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -1,7 +1,5 @@ module Ci class ApplicationController < ::ApplicationController - before_action :check_enable_flag! - def self.railtie_helpers_paths "app/helpers/ci" end @@ -10,13 +8,6 @@ module Ci private - def check_enable_flag! - unless current_application_settings.ci_enabled - redirect_to(disabled_ci_projects_path) - return - end - end - def authenticate_public_page! unless project.public authenticate_user! diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index e8788955eb..82fb14c427 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -3,17 +3,13 @@ module Ci before_action :authenticate_user!, except: [:build, :badge, :show] before_action :authenticate_public_page!, only: :show before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml] - before_action :authorize_access_project!, except: [:build, :badge, :show, :new, :disabled] + before_action :authorize_access_project!, except: [:build, :badge, :show, :new] before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] before_action :authenticate_token!, only: [:build] before_action :no_cache, only: [:badge] - skip_before_action :check_enable_flag!, only: [:disabled] protect_from_forgery except: :build - layout 'ci/project', except: [:index, :disabled] - - def disabled - end + layout 'ci/project', except: [:index] def show @ref = params[:ref] diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 784f5c96a0..c8841178e9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -83,8 +83,7 @@ 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'], - ci_enabled: Settings.gitlab_ci['enabled'] + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] ) end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 143cd10c54..a36ae0b766 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -124,14 +124,5 @@ = 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 :ci_enabled do - = f.check_box :ci_enabled - Disable to prevent CI usage until rake ci:migrate is run (8.0 only) - .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/projects/disabled.html.haml b/app/views/ci/projects/disabled.html.haml deleted file mode 100644 index 83b0d8329e..0000000000 --- a/app/views/ci/projects/disabled.html.haml +++ /dev/null @@ -1 +0,0 @@ -Continuous Integration has been disabled for time of the migration. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4e4a8ecbdb..4c78bd6e2f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -178,7 +178,6 @@ Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious' # CI # Settings['gitlab_ci'] ||= Settingslogic.new({}) -Settings.gitlab_ci['enabled'] = true if Settings.gitlab_ci['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) diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb new file mode 100644 index 0000000000..be6aa810bb --- /dev/null +++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb @@ -0,0 +1,5 @@ +class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 72609da93f..f8ae6893df 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: 20151005162154) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -46,7 +46,6 @@ ActiveRecord::Schema.define(version: 20150930095736) do t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" t.text "help_page_text" - t.boolean "ci_enabled", default: true, null: false end create_table "audit_events", force: true do |t| diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 5109c84e0e..218d8c3adc 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -23,10 +23,6 @@ module Ci rack_response({ 'message' => '500 Internal Server Error' }, 500) end - before do - check_enable_flag! - end - format :json helpers Helpers diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 8e893aa5cc..e602cda81d 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -3,12 +3,6 @@ module Ci module Helpers UPDATE_RUNNER_EVERY = 60 - def check_enable_flag! - unless current_application_settings.ci_enabled - render_api_error!('400 (Bad request) CI is disabled', 400) - end - end - def authenticate_runners! forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN end From 01fe901e47895df923235b66d7e8ce0f2341ee2b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 5 Oct 2015 09:59:02 -0700 Subject: [PATCH 194/543] Fixes GDK issue where repos would not be imported properly Seed-Fu runs this entire fixture in a transaction, so the `after_commit` hook won't run until after the fixture is loaded. That is too late since the Sidekiq::Testing block has already exited. Force clearing the `after_commit` queue to ensure the job is run now. See: gitlab-org/gitlab-development-kit#58 --- db/fixtures/development/04_project.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 8f71198e47..973b3f278f 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -73,6 +73,11 @@ Sidekiq::Testing.inline! do } project = Projects::CreateService.new(User.first, params).execute + # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` + # hook won't run until after the fixture is loaded. That is too late + # since the Sidekiq::Testing block has already exited. Force clearing + # the `after_commit` queue to ensure the job is run now. + project.send(:_run_after_commit_queue) if project.valid? print '.' From a2af080a06b3439d37258d88ac6d6db9ef51c6a5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 14:19:46 -0400 Subject: [PATCH 195/543] Update CHANGELOG for 8.0.4 [ci skip] --- CHANGELOG | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3144175db8..12e0004009 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) - 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) - - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (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) - Show CI status on all pages where commits list is rendered @@ -29,11 +28,17 @@ v 8.1.0 (unreleased) - Ensure code blocks are properly highlighted after a note is updated - Fix wrong access level badge on MR comments - Hide password in the service settings form - - Fix anchors to comments in diffs - 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) +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 + - Fix anchors to comments in diffs + - Remove CI token from build traces + - Fix "Assign All" button on Runner admin page + v 8.0.3 - Fix URL shown in Slack notifications - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) From e5e6c846ead9ea4cacc53c2308c5ca86ce1ab766 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 20:20:27 +0200 Subject: [PATCH 196/543] Fix builds view --- app/views/ci/builds/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml index c42d11bf05..be33c5e8aa 100644 --- a/app/views/ci/builds/show.html.haml +++ b/app/views/ci/builds/show.html.haml @@ -1,7 +1,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu - - @commit.builds_without_retry_for_ref(build.ref).each do |build| + - @commit.builds_without_retry_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to ci_project_build_url(@project, build) do = ci_icon_for_status(build.status) From fc795d6ee2bdc1229f82c535222752364b5c9e44 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 5 Oct 2015 21:20:54 +0200 Subject: [PATCH 197/543] Fix graphical glitches --- app/controllers/ci/projects_controller.rb | 8 ++++--- app/views/ci/commits/_commit.html.haml | 3 ++- app/views/ci/commits/show.html.haml | 26 +++++++++++++++-------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 33b8ae6465..64d544acfd 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -14,9 +14,11 @@ module Ci def show @ref = params[:ref] - @commits = @project.commits.reverse_order - # TODO: this is broken - # @commits = @commits.where(ref: @ref) if @ref + @commits = @project.commits.group(:sha).reverse_order + if @ref + builds = @project.builds.where(ref: @ref).select(:commit_id).distinct + @commits = @commits.where(id: builds) + end @commits = @commits.page(params[:page]).per(20) end diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml index f8a1fa5085..6e6cc9b2c3 100644 --- a/app/views/ci/commits/_commit.html.haml +++ b/app/views/ci/commits/_commit.html.haml @@ -16,7 +16,8 @@ %td.build-branch - unless @ref %span - = link_to truncate(commit.last_ref, length: 25), ci_project_path(@project, ref: commit.last_ref) + - commit.refs.each do |ref| + = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref) %td.duration - if commit.duration > 0 diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml index 7217671fe9..7ebef8c5e0 100644 --- a/app/views/ci/commits/show.html.haml +++ b/app/views/ci/commits/show.html.haml @@ -4,14 +4,22 @@ .gray-content-block.middle-block %pre.commit-message - #{@commit.git_commit_message} + - if @commit.git_commit_message + #{@commit.git_commit_message} + - else + No commit message .gray-content-block.second-block .row .col-sm-6 - %p - %span.attr-name Commit: - #{gitlab_commit_link(@project, @commit.sha)} + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} + %p + - if @commit.refs.present? + %span.attr-name Refs: + - @commit.refs.each do |ref| + #{gitlab_ref_link(@project, ref)} .col-sm-6 - if @commit.git_author_name || @commit.git_author_email %p @@ -22,10 +30,10 @@ %span.attr-name Created at: #{@commit.created_at.to_s(:short)} -- if current_user && can?(current_user, :manage_builds, gl_project) - .pull-right - - if @commit.builds.running_or_pending.any? - = link_to "Cancel", cancel_ci_project_commits_path(@project, @commit), class: 'btn btn-sm btn-danger' + - if current_user && can?(current_user, :manage_builds, gl_project) + - if @commit.builds.running_or_pending.any? + .pull-right + = link_to "Cancel", cancel_ci_project_commits_path(@project, @commit), class: 'btn btn-sm btn-danger' - if @commit.yaml_errors.present? @@ -41,7 +49,7 @@ - @commit.refs.each do |ref| %h3 - Builds for #{ref} + Builds for #{gitlab_ref_link(@project, ref)} - if @commit.duration_for_ref(ref) > 0 %small.pull-right %i.fa.fa-time From e6ab50a754d67ff38e1d358b2222b6db3dbdee80 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 10:34:36 +0200 Subject: [PATCH 198/543] Use CI commit status for merge request widget For temporary compatibility with other services like Jenkins we ask for CI status via AJAX request if there is no commit status in GitLab database Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + .../merge_requests/widget/_heading.html.haml | 58 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3144175db8..04e3807451 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,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 v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 10640f746f..68dda1424c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,30 +1,44 @@ - if @merge_request.has_ci? - .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") + - 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 - - if ci_build_details_path(@merge_request) - = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build details", ci_status_path(ci_commit) - .ci_widget - = icon("spinner spin") - Checking CI status for #{@merge_request.last_commit_short_sha}… + - 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.ci-not_found{style: "display:none"} - = icon("times-circle") - Could not find 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-error{style: "display:none"} - = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again. + .ci_widget.ci-not_found{style: "display:none"} + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. - :coffeescript - $ -> - merge_request_widget.getCiStatus() + .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() From 1a86a00ca338811a0ffb5dd9a4d2a2ec84db13e6 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 5 Oct 2015 18:10:22 +0300 Subject: [PATCH 199/543] Fix search in Files --- CHANGELOG | 1 + app/assets/javascripts/tree.js.coffee | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 12e0004009..5eea7da4af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 8.0.4 - Fix anchors to comments in diffs - Remove CI token from build traces - Fix "Assign All" button on Runner admin page + - Fix search in Files v 8.0.3 - Fix URL shown in Slack notifications diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee index d428db5b42..de8eebcd0b 100644 --- a/app/assets/javascripts/tree.js.coffee +++ b/app/assets/javascripts/tree.js.coffee @@ -16,6 +16,9 @@ class @TreeView li = $("tr.tree-item") liSelected = null $('body').keydown (e) -> + if $("input:focus").length > 0 && (e.which == 38 || e.which == 40) + return false + if e.which is 40 if liSelected next = liSelected.next() @@ -38,4 +41,4 @@ class @TreeView $(liSelected).focus() else if e.which is 13 path = $('.tree-item.selected .tree-item-file-name a').attr('href') - Turbolinks.visit(path) + if path then Turbolinks.visit(path) From a266752821116e4736c493ad865e5ba6e1821c0d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 6 Oct 2015 11:59:55 +0200 Subject: [PATCH 200/543] Latest builds always include builds with unique name and unique ref --- 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 f35224916e..3c92710968 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -47,7 +47,7 @@ module Ci scope :failed, ->() { where(status: "failed") } scope :unstarted, ->() { where(runner_id: nil) } scope :running_or_pending, ->() { where(status:[:running, :pending]) } - scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name)).order(stage_idx: :asc) } + scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :for_ref, ->(ref) { where(ref: ref) } scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } From 065fe557f7c1ec8a520b7c18c83fa062f90d1443 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 6 Oct 2015 12:00:15 +0200 Subject: [PATCH 201/543] Fixed failure reading .gitlab-ci.yml --- app/models/ci/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 46370034f9..fde754a92a 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -211,7 +211,7 @@ module Ci end def ci_yaml_file - gl_project.repository.blob_at(sha, '.gitlab-ci.yml') + gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data rescue nil end From 697b34d786bcbc9b4fdaef485ff4837b850ca5aa Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 13:12:21 +0200 Subject: [PATCH 202/543] Render CI statuses on commit page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/commit.scss | 13 ++++ app/controllers/projects/commit_controller.rb | 7 +++ app/views/projects/commit/_ci_menu.html.haml | 7 +++ app/views/projects/commit/ci.html.haml | 62 +++++++++++++++++++ app/views/projects/commit/show.html.haml | 1 + config/routes.rb | 5 +- features/project/commits/commits.feature | 2 + features/steps/project/commits/commits.rb | 14 ++++- 8 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 app/views/projects/commit/_ci_menu.html.haml create mode 100644 app/views/projects/commit/ci.html.haml diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 741ff9051a..fbd7c363de 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -107,3 +107,16 @@ z-index: 2; } } + +.commit-ci-menu { + padding: 0; + margin: 0; + list-style: none; + margin-top: 5px; + height: 56px; + margin: -16px; + padding: 16px; + text-align: center; + margin-top: 0px; + margin-bottom: 2px; +} diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 2fae505713..1938c63c10 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -31,6 +31,13 @@ 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 + @ci_project = @project.gitlab_ci_project + end + def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml new file mode 100644 index 0000000000..a634ae5dfd --- /dev/null +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -0,0 +1,7 @@ +%ul.center-top-menu.commit-ci-menu + = 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 + Builds diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml new file mode 100644 index 0000000000..bbb41a8026 --- /dev/null +++ b/app/views/projects/commit/ci.html.haml @@ -0,0 +1,62 @@ +- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/header_title" += render "commit_box" += render "ci_menu" + +- if @ci_project && current_user && can?(current_user, :manage_builds, @project) + .pull-right + - if @ci_commit.builds.running_or_pending.any? + = link_to "Cancel", cancel_ci_project_commits_path(@ci_project, @ci_commit), class: 'btn btn-sm btn-danger' + + +- if @ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @ci_commit.yaml_errors.split(",").each do |error| + %li= error + +- unless @ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +- @ci_commit.refs.each do |ref| + .gray-content-block.second-block + Builds for #{ref} + - if @ci_commit.duration_for_ref(ref) > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @ci_commit.duration_for_ref(ref)} + + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + = render partial: "ci/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true + +- if @ci_commit.retried_builds.any? + %h3 + Retried builds + + %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: "ci/builds/build", collection: @ci_commit.retried_builds, ref: true diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index f8681024d1..30a3973828 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,5 +1,6 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" = render "commit_box" += render "ci_menu" if @ci_commit = render "projects/diffs/diffs", diffs: @diffs, project: @project = render "projects/notes/notes_with_form", view: params[:view] diff --git a/config/routes.rb b/config/routes.rb index a21889631e..beebb3258d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -485,7 +485,10 @@ Gitlab::Application.routes.draw do resource :avatar, only: [:show, :destroy] resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do - get :branches, on: :member + member do + get :branches + get :ci + end end resources :compare, only: [:index, :create] diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 34161b81d4..e4beeb59ad 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -20,6 +20,8 @@ Feature: Project Commits Given commit has ci status And I click on commit link Then I see commit ci info + And I click status link + Then I see builds list Scenario: I browse commit with side-by-side diff view Given I click on commit link diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 5ebc3a4976..ae5f90004e 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -104,10 +104,20 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci - create :ci_commit, gl_project: @project, sha: sample_commit.id + ci_commit = create :ci_commit, gl_project: @project, sha: sample_commit.id + create :ci_build, commit: ci_commit end step 'I see commit ci info' do - expect(page).to have_content "build: skipped" + expect(page).to have_content "build: pending" + end + + step 'I click status link' do + click_link "Builds" + end + + step 'I see builds list' do + expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for master" end end From f509e3afeb47158558e417d700bfc42db5ddd3f5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 16:21:19 +0200 Subject: [PATCH 203/543] Link ci_status_path to new ci commit page Signed-off-by: Dmitriy Zaporozhets --- app/helpers/ci_status_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 794bdc2530..dbd1e26fa7 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,6 +1,7 @@ module CiStatusHelper def ci_status_path(ci_commit) - ci_project_commits_path(ci_commit.project, ci_commit) + project = ci_commit.gl_project + ci_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end def ci_status_icon(ci_commit) From 17de909a42fa5d641eb9554e94fee59fb0762cbc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 6 Oct 2015 16:25:27 +0200 Subject: [PATCH 204/543] Fix broken grouping sql clause when rendering commits for CI --- app/controllers/ci/projects_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 64d544acfd..adb913a783 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -14,9 +14,10 @@ module Ci def show @ref = params[:ref] - @commits = @project.commits.group(:sha).reverse_order + @commits = @project.commits.reverse_order if @ref - builds = @project.builds.where(ref: @ref).select(:commit_id).distinct + # unscope is required, because of default_scope defined in Ci::Build + builds = @project.builds.unscope(:select, :order).where(ref: @ref).select(:commit_id).distinct @commits = @commits.where(id: builds) end @commits = @commits.page(params[:page]).per(20) From 1f14e689e579163222fa0a410d3ab4ca672d465e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Oct 2015 13:52:11 +0200 Subject: [PATCH 205/543] Added specs for TrendingProjectsFinder --- spec/finders/trending_projects_finder_spec.rb | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 spec/finders/trending_projects_finder_spec.rb diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb new file mode 100644 index 0000000000..1ad2e23f1f --- /dev/null +++ b/spec/finders/trending_projects_finder_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe TrendingProjectsFinder do + let(:user) { create(:user) } + let(:group) { create(:group) } + + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :public, group: group) } + + before do + 2.times do + create(:note_on_commit, project: project1) + end + + create(:note_on_commit, project: project2) + end + + describe '#execute' do + describe 'without an explicit start date' do + subject { described_class.new.execute(user).to_a } + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project1, project2]) + end + end + + describe 'with an explicit start date' do + let(:date) { 2.months.ago } + + subject { described_class.new.execute(user, date).to_a } + + before do + 2.times do + create(:note_on_commit, project: project2, created_at: date) + end + end + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project2, project1]) + end + end + end +end From d15eec64604d68093d8f01711c1847ee240eb0d2 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Oct 2015 13:52:19 +0200 Subject: [PATCH 206/543] Use >= instead of > in TrendingProjectsFinder By using >= we can ensure we actually get all comments of the past month, instead of the comments of the past month minus the first day in the range. --- app/finders/trending_projects_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 9ea342cb26..9b710f0751 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -6,7 +6,7 @@ class TrendingProjectsFinder # Determine trending projects based on comments count # for period of time - ex. month - projects.joins(:notes).where('notes.created_at > ?', start_date). + projects.joins(:notes).where('notes.created_at >= ?', start_date). group("projects.id").reorder("count(notes.id) DESC") end From b7abba0ca0a4629a854eee0488f94f160452e2f6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Oct 2015 16:35:51 +0200 Subject: [PATCH 207/543] Revamp trending projects query This changes the query to use a COUNT nested in an INNER JOIN, instead of a COUNT plus a GROUP BY. There are two reasons for this: 1. Using a COUNT in an INNER JOIN can be quite a bit faster. 2. The use of a GROUP BY means that method calls such as "any?" (and everything else that calls "count") operate on a Hash that counts the amount of notes on a per project basis, instead of just counting the total amount of projects. The query has been moved into Project.trending as its logic is simple enough. As a result of this testing the TrendingProjectsFinder class simply involves testing if the right methods are called, removing the need for setting up database records. --- app/finders/trending_projects_finder.rb | 11 +---- app/models/project.rb | 14 ++++++ spec/finders/trending_projects_finder_spec.rb | 44 +++++++++---------- spec/models/project_spec.rb | 38 ++++++++++++++++ 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 9b710f0751..81a1240380 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -1,13 +1,6 @@ class TrendingProjectsFinder - def execute(current_user, start_date = nil) - start_date ||= Date.today - 1.month - - projects = projects_for(current_user) - - # Determine trending projects based on comments count - # for period of time - ex. month - projects.joins(:notes).where('notes.created_at >= ?', start_date). - group("projects.id").reorder("count(notes.id) DESC") + def execute(current_user, start_date = 1.month.ago) + projects_for(current_user).trending(start_date) end private diff --git a/app/models/project.rb b/app/models/project.rb index bb47b9abb0..4661522b8a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -260,6 +260,20 @@ class Project < ActiveRecord::Base name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR %r{(?#{name_pattern}/#{name_pattern})} end + + def trending(since = 1.month.ago) + # By counting in the JOIN we don't expose the GROUP BY to the outer query. + # This means that calls such as "any?" and "count" just return a number of + # the total count, instead of the counts grouped per project as a Hash. + join_body = "INNER JOIN ( + SELECT project_id, COUNT(*) AS amount + FROM notes + WHERE created_at >= #{sanitize(since)} + GROUP BY project_id + ) join_note_counts ON projects.id = join_note_counts.project_id" + + joins(join_body).reorder('join_note_counts.amount DESC') + end end def team diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb index 1ad2e23f1f..a49cbfd516 100644 --- a/spec/finders/trending_projects_finder_spec.rb +++ b/spec/finders/trending_projects_finder_spec.rb @@ -1,42 +1,38 @@ require 'spec_helper' describe TrendingProjectsFinder do - let(:user) { create(:user) } - let(:group) { create(:group) } - - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :public, group: group) } - - before do - 2.times do - create(:note_on_commit, project: project1) - end - - create(:note_on_commit, project: project2) - end + let(:user) { build(:user) } describe '#execute' do describe 'without an explicit start date' do - subject { described_class.new.execute(user).to_a } + subject { described_class.new } - it 'sorts Projects by the amount of notes in descending order' do - expect(subject).to eq([project1, project2]) + it 'returns the trending projects' do + relation = double(:ar_relation) + + allow(subject).to receive(:projects_for) + .with(user) + .and_return(relation) + + allow(relation).to receive(:trending) + .with(an_instance_of(ActiveSupport::TimeWithZone)) end end describe 'with an explicit start date' do let(:date) { 2.months.ago } - subject { described_class.new.execute(user, date).to_a } + subject { described_class.new } - before do - 2.times do - create(:note_on_commit, project: project2, created_at: date) - end - end + it 'returns the trending projects' do + relation = double(:ar_relation) - it 'sorts Projects by the amount of notes in descending order' do - expect(subject).to eq([project2, project1]) + allow(subject).to receive(:projects_for) + .with(user) + .and_return(relation) + + allow(relation).to receive(:trending) + .with(date) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8b5d2c3a1c..f93935ebe3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -423,4 +423,42 @@ describe Project do it { expect(project.gitlab_ci?).to be_truthy } it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } end + + describe '.trending' do + let(:group) { create(:group) } + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :public, group: group) } + + before do + 2.times do + create(:note_on_commit, project: project1) + end + + create(:note_on_commit, project: project2) + end + + describe 'without an explicit start date' do + subject { described_class.trending.to_a } + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project1, project2]) + end + end + + describe 'with an explicit start date' do + let(:date) { 2.months.ago } + + subject { described_class.trending(date).to_a } + + before do + 2.times do + create(:note_on_commit, project: project2, created_at: date) + end + end + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project2, project1]) + end + end + end end From 7adf9a52ba94433f7de30f190a2d94bc3d7949fd Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Oct 2015 16:39:03 +0200 Subject: [PATCH 208/543] Added benchmarks for finding trending projects --- .../finders/trending_projects_finder_spec.rb | 14 ++++++++ spec/benchmarks/models/project_spec.rb | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 spec/benchmarks/finders/trending_projects_finder_spec.rb create mode 100644 spec/benchmarks/models/project_spec.rb diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb new file mode 100644 index 0000000000..551ce21840 --- /dev/null +++ b/spec/benchmarks/finders/trending_projects_finder_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe TrendingProjectsFinder, benchmark: true do + describe '#execute' do + let(:finder) { described_class.new } + let(:user) { create(:user) } + + # to_a is used to force actually running the query (instead of just building + # it). + benchmark_subject { finder.execute(user).non_archived.to_a } + + it { is_expected.to iterate_per_second(500) } + end +end diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb new file mode 100644 index 0000000000..f1dd10440a --- /dev/null +++ b/spec/benchmarks/models/project_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Project, benchmark: true do + describe '.trending' do + let(:group) { create(:group) } + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :public, group: group) } + + let(:iterations) { 500 } + + before do + 2.times do + create(:note_on_commit, project: project1) + end + + create(:note_on_commit, project: project2) + end + + describe 'without an explicit start date' do + benchmark_subject { described_class.trending.to_a } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'with an explicit start date' do + let(:date) { 1.month.ago } + + benchmark_subject { described_class.trending(date).to_a } + + it { is_expected.to iterate_per_second(iterations) } + end + end +end From 4841e883020146fecd386caff7f31f2d9cf307b6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Oct 2015 17:32:22 +0200 Subject: [PATCH 209/543] Added changelog entry for trending projects tweaks --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0400dbfabe..ea0f6909f5 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) + - Improved performance of the trending projects page - 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) - Add user preference to view activities as default dashboard (Stan Hu) From f6223ffb318d2e81ec50037810d23bbd86a400e8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 17:11:10 +0200 Subject: [PATCH 210/543] Move CI build page to CE project Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/ci/builds.scss | 8 +- app/assets/stylesheets/generic/common.scss | 4 + app/controllers/projects/builds_controller.rb | 25 +++ app/views/projects/builds/_build.html.haml | 50 ++++++ app/views/projects/builds/show.html.haml | 159 ++++++++++++++++++ app/views/projects/commit/ci.html.haml | 4 +- config/routes.rb | 2 + 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 app/controllers/projects/builds_controller.rb create mode 100644 app/views/projects/builds/_build.html.haml create mode 100644 app/views/projects/builds/show.html.haml diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss index a11a935b54..a27dd0db58 100644 --- a/app/assets/stylesheets/ci/builds.scss +++ b/app/assets/stylesheets/ci/builds.scss @@ -1,4 +1,4 @@ -.ci-body { +.build-page { pre.trace { background: #111111; color: #fff; @@ -67,4 +67,10 @@ color: #3084bb !important; } } + + .build-top-menu { + margin-top: 0; + margin-bottom: 2px; + } } + diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 016cc015e9..45e284542d 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -381,6 +381,10 @@ table { &.no-bottom { margin-bottom: 0; } + + &.no-top { + margin-top: 0; + } } .dropzone .dz-preview .dz-progress { diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb new file mode 100644 index 0000000000..9bb1a9e14e --- /dev/null +++ b/app/controllers/projects/builds_controller.rb @@ -0,0 +1,25 @@ +class Projects::BuildsController < Projects::ApplicationController + before_action :ci_project + before_action :build + + layout "project" + + 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) + @commit = @build.commit + + respond_to do |format| + format.html + format.json do + render json: @build.to_json(methods: :trace_html) + end + end + end + + private + + def build + @build ||= ci_project.builds.unscoped.find_by(id: params[:id]) + end +end diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml new file mode 100644 index 0000000000..21c543b38d --- /dev/null +++ b/app/views/projects/builds/_build.html.haml @@ -0,0 +1,50 @@ +- gl_project = build.project.gl_project +%tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.build-link + = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do + %strong Build ##{build.id} + + - if defined?(ref) + %td + = build.ref + + %td + = build.stage + + %td + = build.name + .pull-right + - if build.tags.any? + - build.tag_list.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 + + - if build.project.coverage_enabled? + %td.coverage + - if build.coverage + #{build.coverage}% + + %td + - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if build.active? + = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do + %i.fa.fa-remove.cred + - elsif build.commands.present? + = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do + %i.fa.fa-repeat diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml new file mode 100644 index 0000000000..93cd4dcfd9 --- /dev/null +++ b/app/views/projects/builds/show.html.haml @@ -0,0 +1,159 @@ +.build-page + .gray-content-block + Build for commit + %strong.monospace + = link_to @build.commit.short_sha, ci_status_path(@build.commit) + from + %code #{@build.ref} + + #up-build-trace + - if @commit.matrix_for_ref?(@build.ref) + %ul.center-top-menu.build-top-menu + - @commit.builds_without_retry_for_ref(@build.ref).each do |build| + %li{class: ('active' if build == @build) } + = link_to namespace_project_build_path(@project.namespace, @project, build) do + = ci_icon_for_status(build.status) + %span + - if build.name + = build.name + - else + = build.id + + + - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) + %li.active + %a + Build ##{@build.id} + · + %i.fa.fa-warning-sign + This build was retried. + + .gray-content-block.second-block + .build-head + .clearfix + = ci_status_with_icon(@build.status) + - if @build.duration + %span + %i.fa.fa-time + #{duration_in_words(@build.finished_at, @build.started_at)} + .pull-right + = @build.updated_at.stamp('19:00 Aug 27') + + .row.prepend-top-default + .col-md-9 + .clearfix + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + .clearfix + .scroll-controls + = link_to '#up-build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + + %pre.trace#build-trace + %code.bash + = preserve do + = raw @build.trace_html + %div#down-build-trace + + .col-md-3 + - if @build.coverage + .build-widget + %h4.title + Test coverage + %h1 #{@build.coverage}% + + + .build-widget + %h4.title + Build + - if current_user && can?(current_user, :manage_builds, @project) + .pull-right + - if @build.active? + = link_to "Cancel", cancel_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-danger' + - elsif @build.commands.present? + = link_to "Retry", retry_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-primary', method: :post + + - if @build.duration + %p + %span.attr-name Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + %p + %span.attr-name Created: + #{time_ago_in_words(@build.created_at)} ago + - if @build.finished_at + %p + %span.attr-name Finished: + #{time_ago_in_words(@build.finished_at)} ago + %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)} + - elsif @build.runner + \##{@build.runner.id} + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.attr-name Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.attr-name Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .build-widget + %h4.title + Commit + .pull-right + %small #{build_commit_link @build} + %p + %span.attr-name Branch: + #{build_ref_link @build} + %p + %span.attr-name Author: + #{@build.commit.git_author_name} + %p + %span.attr-name Message: + #{@build.commit.git_commit_message} + + - if @build.tags.any? + .build-widget + %h4.title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag + + - if @builds.present? + .build-widget + %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %table.table.builds + - @builds.each_with_index do |build, i| + %tr.build + %td + = ci_icon_for_status(build.status) + %td + = link_to namespace_project_build_path(@project.namespace, @project, @build) do + - if build.name + = build.name + - else + %span ##{build.id} + + %td.status= build.status + + + = paginate @builds + + + :javascript + new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}") diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index bbb41a8026..f4382e8804 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -40,7 +40,7 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "ci/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true + = render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true - if @ci_commit.retried_builds.any? %h3 @@ -59,4 +59,4 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "ci/builds/build", collection: @ci_commit.retried_builds, ref: true + = render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true diff --git a/config/routes.rb b/config/routes.rb index beebb3258d..7f7c918ea5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -592,6 +592,8 @@ Gitlab::Application.routes.draw do end end + resources :builds, only: [:show] + resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do get :test From 04c7dc2a9e2385ce47a70205eafd4cf4f91a0bba Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 20:15:06 +0200 Subject: [PATCH 211/543] Cleanup CI code after refactoring and fix several 500 errors Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/builds_controller.rb | 40 +---- app/controllers/ci/commits_controller.rb | 6 - app/controllers/projects/builds_controller.rb | 2 +- app/helpers/application_helper.rb | 4 + app/helpers/ci/commits_helper.rb | 24 --- app/views/ci/admin/builds/_build.html.haml | 6 +- app/views/ci/admin/runners/show.html.haml | 10 +- app/views/ci/builds/_build.html.haml | 49 ------ app/views/ci/builds/show.html.haml | 165 ------------------ app/views/ci/commits/_commit.html.haml | 2 +- app/views/ci/commits/show.html.haml | 89 ---------- app/views/layouts/ci/build.html.haml | 11 -- app/views/layouts/ci/commit.html.haml | 11 -- config/routes.rb | 6 +- 14 files changed, 25 insertions(+), 400 deletions(-) delete mode 100644 app/helpers/ci/commits_helper.rb delete mode 100644 app/views/ci/builds/_build.html.haml delete mode 100644 app/views/ci/builds/show.html.haml delete mode 100644 app/views/ci/commits/show.html.haml delete mode 100644 app/views/layouts/ci/build.html.haml delete mode 100644 app/views/layouts/ci/commit.html.haml diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb index bf87f81439..daf3bcf72a 100644 --- a/app/controllers/ci/builds_controller.rb +++ b/app/controllers/ci/builds_controller.rb @@ -1,41 +1,11 @@ module Ci class BuildsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:status, :show] - before_action :authenticate_public_page!, only: :show + before_action :authenticate_user!, except: [:status] before_action :project - before_action :authorize_access_project!, except: [:status, :show] - before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel] + before_action :authorize_access_project!, except: [:status] + before_action :authorize_manage_project!, except: [:status, :retry, :cancel] before_action :authorize_manage_builds!, only: [:retry, :cancel] - before_action :build, except: [:show] - layout 'ci/build' - - def show - if params[:id] =~ /\A\d+\Z/ - @build = build - else - # try to find commit by sha - commit = commit_by_sha - - if commit - # Redirect to commit page - redirect_to ci_project_commit_path(@project, @build.commit) - return - end - end - - raise ActiveRecord::RecordNotFound unless @build - - @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC') - @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) - @commit = @build.commit - - respond_to do |format| - format.html - format.json do - render json: @build.to_json(methods: :trace_html) - end - end - end + before_action :build def retry if @build.commands.blank? @@ -68,7 +38,7 @@ module Ci end def build - @build ||= project.builds.unscoped.find_by(id: params[:id]) + @build ||= project.builds.unscoped.find_by!(id: params[:id]) end def commit_by_sha diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb index 887e92f84c..404e0e4b41 100644 --- a/app/controllers/ci/commits_controller.rb +++ b/app/controllers/ci/commits_controller.rb @@ -5,12 +5,6 @@ module Ci before_action :project before_action :authorize_access_project!, except: [:status, :show, :cancel] before_action :authorize_manage_builds!, only: [:cancel] - before_action :commit, only: :show - layout 'ci/commit' - - def show - @builds = @commit.builds - end def status commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 9bb1a9e14e..76c7f31f61 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -20,6 +20,6 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= ci_project.builds.unscoped.find_by(id: params[:id]) + @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3ab44719d9..cab2278adb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -314,4 +314,8 @@ module ApplicationHelper html.html_safe end + + def truncate_first_line(message, length = 50) + truncate(message.each_line.first.chomp, length: length) if message + end end diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb deleted file mode 100644 index a0df4c3d72..0000000000 --- a/app/helpers/ci/commits_helper.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Ci - module CommitsHelper - def ci_commit_path(commit) - ci_project_commits_path(commit.project, commit) - end - - def commit_link(commit) - link_to(commit.short_sha, ci_commit_path(commit)) - end - - def truncate_first_line(message, length = 50) - truncate(message.each_line.first.chomp, length: length) if message - end - - def ci_commit_title(commit) - content_tag :span do - link_to( - simple_sanitize(commit.project.name), ci_project_path(commit.project) - ) + ' @ ' + - gitlab_commit_link(@project, @commit.sha) - end - end - end -end diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml index 778d51d03b..2df5871321 100644 --- a/app/views/ci/admin/builds/_build.html.haml +++ b/app/views/ci/admin/builds/_build.html.haml @@ -1,14 +1,16 @@ +- gl_project = build.project.gl_project - if build.commit && build.project %tr.build %td.build-link - = link_to ci_project_build_url(build.project, build) do + = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do %strong #{build.id} %td.status = ci_status_with_icon(build.status) %td.commit-link - = commit_link(build.commit) + = link_to ci_status_path(build.commit) do + %strong #{build.commit.short_sha} %td.runner - if build.runner diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 09905e0eb4..5bb442cbf9 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -96,6 +96,7 @@ %table.builds.runner-builds %thead %tr + %th Build ID %th Status %th Project %th Commit @@ -103,6 +104,11 @@ - @builds.each do |build| %tr.build + %td.id + - gl_project = build.project.gl_project + = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do + = build.id + %td.status = ci_status_with_icon(build.status) @@ -110,8 +116,8 @@ = build.project.name %td.build-link - = link_to ci_project_build_path(build.project, build) do - %strong #{build.short_sha} + = link_to ci_status_path(build.commit) do + %strong #{build.commit.short_sha} %td.timestamp - if build.finished_at diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml deleted file mode 100644 index 8ccc0dff2f..0000000000 --- a/app/views/ci/builds/_build.html.haml +++ /dev/null @@ -1,49 +0,0 @@ -%tr.build - %td.status - = ci_status_with_icon(build.status) - - %td.build-link - = link_to ci_project_build_path(build.project, build) do - %strong Build ##{build.id} - - - if defined?(ref) - %td - = build.ref - - %td - = build.stage - - %td - = build.name - .pull-right - - if build.tags.any? - - build.tag_list.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 - - - if build.project.coverage_enabled? - %td.coverage - - if build.coverage - #{build.coverage}% - - %td - - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) - .pull-right - - if build.active? - = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do - %i.fa.fa-remove.cred - - elsif build.commands.present? - = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do - %i.fa.fa-repeat diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml deleted file mode 100644 index be33c5e8aa..0000000000 --- a/app/views/ci/builds/show.html.haml +++ /dev/null @@ -1,165 +0,0 @@ -#up-build-trace -- if @commit.matrix_for_ref?(@build.ref) - %ul.center-top-menu - - @commit.builds_without_retry_for_ref(@build.ref).each do |build| - %li{class: ('active' if build == @build) } - = link_to ci_project_build_url(@project, build) do - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - - - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) - %li.active - %a - Build ##{@build.id} - · - %i.fa.fa-warning-sign - This build was retried. - -.gray-content-block - .build-head - %h4 - - if @build.commit.tag? - Build for tag - %code #{@build.ref} - - else - Build for commit - %strong.monospace= commit_link(@build.commit) - from - - = link_to ci_project_path(@build.project, ref: @build.ref) do - %strong.monospace= "#{@build.ref}" - - - if @build.duration - .pull-right - %span - %i.fa.fa-time - #{duration_in_words(@build.finished_at, @build.started_at)} - - .clearfix - = ci_status_with_icon(@build.status) - .pull-right - = @build.updated_at.stamp('19:00 Aug 27') - -.row.prepend-top-default - .col-md-9 - .clearfix - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - .clearfix - .scroll-controls - = link_to '#up-build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down - - %pre.trace#build-trace - %code.bash - = preserve do - = raw @build.trace_html - %div#down-build-trace - - .col-md-3 - - if @build.coverage - .build-widget - %h4.title - Test coverage - %h1 #{@build.coverage}% - - - .build-widget - %h4.title - Build - - if current_user && can?(current_user, :manage_builds, gl_project) - .pull-right - - if @build.active? - = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger' - - elsif @build.commands.present? - = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post - - - if @build.duration - %p - %span.attr-name Duration: - #{duration_in_words(@build.finished_at, @build.started_at)} - %p - %span.attr-name Created: - #{time_ago_in_words(@build.created_at)} ago - - if @build.finished_at - %p - %span.attr-name Finished: - #{time_ago_in_words(@build.finished_at)} ago - %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)} - - elsif @build.runner - \##{@build.runner.id} - - - if @build.trigger_request - .build-widget - %h4.title - Trigger - - %p - %span.attr-name Token: - #{@build.trigger_request.trigger.short_token} - - - if @build.trigger_request.variables - %p - %span.attr-name Variables: - - %code - - @build.trigger_request.variables.each do |key, value| - #{key}=#{value} - - .build-widget - %h4.title - Commit - .pull-right - %small #{build_commit_link @build} - %p - %span.attr-name Branch: - #{build_ref_link @build} - %p - %span.attr-name Author: - #{@build.commit.git_author_name} - %p - %span.attr-name Message: - #{@build.commit.git_commit_message} - - - if @build.tags.any? - .build-widget - %h4.title - Tags - - @build.tag_list.each do |tag| - %span.label.label-primary - = tag - - - if @builds.present? - .build-widget - %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: - %table.builds - - @builds.each_with_index do |build, i| - %tr.build - %td - = ci_icon_for_status(build.status) - %td - = link_to ci_project_build_url(@project, build) do - - if build.name - = build.name - - else - %span ##{build.id} - - %td.status= build.status - - - = paginate @builds - - -:javascript - new CiBuild("#{ci_project_build_url(@project, @build)}", "#{@build.status}") diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml index 6e6cc9b2c3..b24a3b826c 100644 --- a/app/views/ci/commits/_commit.html.haml +++ b/app/views/ci/commits/_commit.html.haml @@ -7,7 +7,7 @@ %td.build-link - = link_to ci_project_commits_path(commit.project, commit.sha) do + = link_to ci_status_path(commit) do %strong #{commit.short_sha} %td.build-message diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml deleted file mode 100644 index 7ebef8c5e0..0000000000 --- a/app/views/ci/commits/show.html.haml +++ /dev/null @@ -1,89 +0,0 @@ -.commit-info - .append-bottom-20 - = ci_status_with_icon(@commit.status) - - .gray-content-block.middle-block - %pre.commit-message - - if @commit.git_commit_message - #{@commit.git_commit_message} - - else - No commit message - - .gray-content-block.second-block - .row - .col-sm-6 - %p - %span.attr-name Commit: - #{gitlab_commit_link(@project, @commit.sha)} - %p - - if @commit.refs.present? - %span.attr-name Refs: - - @commit.refs.each do |ref| - #{gitlab_ref_link(@project, ref)} - .col-sm-6 - - if @commit.git_author_name || @commit.git_author_email - %p - %span.attr-name Author: - #{@commit.git_author_name} (#{@commit.git_author_email}) - - if @commit.created_at - %p - %span.attr-name Created at: - #{@commit.created_at.to_s(:short)} - - - if current_user && can?(current_user, :manage_builds, gl_project) - - if @commit.builds.running_or_pending.any? - .pull-right - = link_to "Cancel", cancel_ci_project_commits_path(@project, @commit), class: 'btn btn-sm btn-danger' - - -- if @commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @commit.yaml_errors.split(",").each do |error| - %li= error - -- unless @commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -- @commit.refs.each do |ref| - %h3 - Builds for #{gitlab_ref_link(@project, ref)} - - if @commit.duration_for_ref(ref) > 0 - %small.pull-right - %i.fa.fa-time - #{time_interval_in_words @commit.duration_for_ref(ref)} - - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Stage - %th Name - %th Duration - %th Finished at - - if @project.coverage_enabled? - %th Coverage - %th - = render @commit.builds_without_retry.for_ref(ref), controls: true - -- if @commit.retried_builds.any? - %h3 - Retried builds - - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @project.coverage_enabled? - %th Coverage - %th - = render @commit.retried_builds, ref: true diff --git a/app/views/layouts/ci/build.html.haml b/app/views/layouts/ci/build.html.haml deleted file mode 100644 index a1356f0dc2..0000000000 --- a/app/views/layouts/ci/build.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 ci_commit_title(@commit) - - 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/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml deleted file mode 100644 index a1356f0dc2..0000000000 --- a/app/views/layouts/ci/commit.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 ci_commit_title(@commit) - - 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/config/routes.rb b/config/routes.rb index 7f7c918ea5..ccce40589e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,16 +28,14 @@ Gitlab::Application.routes.draw do end end - resource :charts, only: [:show] - - resources :commits, only: [:show] do + resources :commits, only: [] do member do get :status get :cancel end end - resources :builds, only: [:show] do + resources :builds, only: [] do member do get :cancel get :status From 9ccd8e2617e9d6e8b5f2ef02e46b5ac8457807aa Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 6 Oct 2015 11:21:45 -0700 Subject: [PATCH 212/543] Verify repository is valid --- db/fixtures/development/04_project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 973b3f278f..b463999996 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -79,7 +79,7 @@ Sidekiq::Testing.inline! do # the `after_commit` queue to ensure the job is run now. project.send(:_run_after_commit_queue) - if project.valid? + if project.valid? && project.valid_repo? print '.' else puts project.errors.full_messages From 27b75b2b2d9c274fca005949f645d97ce9333d71 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 20:36:22 +0200 Subject: [PATCH 213/543] Refactor commit/build tests and fix CI cancel Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/commits_controller.rb | 2 +- spec/features/builds_spec.rb | 22 ++++++++++++ spec/features/ci/builds_spec.rb | 28 --------------- spec/features/{ci => }/commits_spec.rb | 45 +++++++----------------- 5 files changed, 36 insertions(+), 62 deletions(-) create mode 100644 spec/features/builds_spec.rb rename spec/features/{ci => }/commits_spec.rb (53%) diff --git a/CHANGELOG b/CHANGELOG index 0400dbfabe..388fa2f896 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.1.0 (unreleased) - 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 + - Integrate CI commit and build pages into project pages 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/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb index 404e0e4b41..7e6705c970 100644 --- a/app/controllers/ci/commits_controller.rb +++ b/app/controllers/ci/commits_controller.rb @@ -16,7 +16,7 @@ module Ci def cancel commit.builds.running_or_pending.each(&:cancel) - redirect_to ci_project_commits_path(project, commit.sha) + redirect_to namespace_project_commit_path(commit.gl_project.namespace, commit.gl_project, commit.sha) end private diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb new file mode 100644 index 0000000000..d0d60491b6 --- /dev/null +++ b/spec/features/builds_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "Builds" do + + before do + login_as(:user) + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + @gl_project = @commit.project.gl_project + @gl_project.team << [@user, :master] + end + + describe "GET /:project/builds/:id" do + before do + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } + end +end diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb index d65699dbef..07f76e4065 100644 --- a/spec/features/ci/builds_spec.rb +++ b/spec/features/ci/builds_spec.rb @@ -9,16 +9,6 @@ describe "Builds" do @commit.project.gl_project.team << [@user, :master] end - describe "GET /:project/builds/:id" do - before do - visit ci_project_build_path(@commit.project, @build) - end - - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } - end - describe "GET /:project/builds/:id/cancel" do before do @build.run! @@ -40,22 +30,4 @@ describe "Builds" do it { expect(page).to have_content 'Cancel' } end end - - context :public_project do - describe "Show page public accessible" do - before do - @commit = FactoryGirl.create :ci_commit - @commit.project.public = true - @commit.project.save - - @runner = FactoryGirl.create :ci_specific_runner - @build = FactoryGirl.create :ci_build, commit: @commit, runner: @runner - - stub_gitlab_calls - visit ci_project_build_path(@commit.project, @build) - end - - it { expect(page).to have_content @commit.sha[0..7] } - end - end end diff --git a/spec/features/ci/commits_spec.rb b/spec/features/commits_spec.rb similarity index 53% rename from spec/features/ci/commits_spec.rb rename to spec/features/commits_spec.rb index b4236e1e58..5da220859e 100644 --- a/spec/features/ci/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,14 +1,17 @@ require 'spec_helper' describe "Commits" do - include Ci::CommitsHelper + include CiStatusHelper - context "Authenticated user" do + let(:project) { create(:project) } + + describe "CI" do before do - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit login_as :user - @commit.project.gl_project.team << [@user, :master] + project.team << [@user, :master] + @ci_project = project.ensure_gitlab_ci_project + @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha + @build = FactoryGirl.create :ci_build, commit: @commit end before do @@ -17,7 +20,7 @@ describe "Commits" do describe "GET /:project/commits/:sha" do before do - visit ci_commit_path(@commit) + visit ci_status_path(@commit) end it { expect(page).to have_content @commit.sha[0..7] } @@ -27,47 +30,23 @@ describe "Commits" do describe "Cancel commit" do it "cancels commit" do - visit ci_commit_path(@commit) + visit ci_status_path(@commit) click_on "Cancel" - expect(page).to have_content "canceled" end end describe ".gitlab-ci.yml not found warning" do it "does not show warning" do - visit ci_commit_path(@commit) - + visit ci_status_path(@commit) expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" end it "shows warning" do stub_ci_commit_yaml_file(nil) - - visit ci_commit_path(@commit) - + visit ci_status_path(@commit) expect(page).to have_content ".gitlab-ci.yml not found in this commit" end end end - - context "Public pages" do - before do - @commit = FactoryGirl.create :ci_commit - @commit.project.public = true - @commit.project.save - - @build = FactoryGirl.create :ci_build, commit: @commit - end - - describe "GET /:project/commits/:sha" do - before do - visit ci_commit_path(@commit) - end - - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } - end - end end From 82b6a17ca70a20015e654b4a0fdb8aec6d244f95 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Oct 2015 21:41:37 +0200 Subject: [PATCH 214/543] Fix ci build routing and few tests Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/builds_controller.rb | 8 +++-- app/helpers/builds_helper.rb | 2 +- app/models/ci/build.rb | 2 +- spec/features/ci/builds_spec.rb | 42 ++++++++++++------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb index daf3bcf72a..b0b8b62fce 100644 --- a/app/controllers/ci/builds_controller.rb +++ b/app/controllers/ci/builds_controller.rb @@ -17,7 +17,7 @@ module Ci if params[:return_to] redirect_to URI.parse(params[:return_to]).path else - redirect_to ci_project_build_path(project, build) + redirect_to build_path(build) end end @@ -28,7 +28,7 @@ module Ci def cancel @build.cancel - redirect_to ci_project_build_path(@project, @build) + redirect_to build_path(@build) end protected @@ -44,5 +44,9 @@ module Ci def commit_by_sha @project.commits.find_by(sha: params[:id]) end + + def build_path(build) + namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) + end end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 626f4e2f4c..1b5a2c31d7 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -8,6 +8,6 @@ module BuildsHelper end def build_url(build) - ci_project_build_url(build.project, build) + namespace_project_build_path(build.gl_project, build.project, build) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3c92710968..5d17f4418e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -144,7 +144,7 @@ module Ci state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :project, + delegate :sha, :short_sha, :project, :gl_project, to: :commit, prefix: false def before_sha diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb index 07f76e4065..aa0df59c04 100644 --- a/spec/features/ci/builds_spec.rb +++ b/spec/features/ci/builds_spec.rb @@ -1,33 +1,31 @@ require 'spec_helper' describe "Builds" do - context :private_project do + before do + login_as(:user) + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + @gl_project = @commit.project.gl_project + @gl_project.team << [@user, :master] + end + + describe "GET /:project/builds/:id/cancel" do before do - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - login_as :user - @commit.project.gl_project.team << [@user, :master] + @build.run! + visit cancel_ci_project_build_path(@commit.project, @build) end - describe "GET /:project/builds/:id/cancel" do - before do - @build.run! - visit cancel_ci_project_build_path(@commit.project, @build) - end + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } + end - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } + describe "POST /:project/builds/:id/retry" do + before do + visit cancel_ci_project_build_path(@commit.project, @build) + click_link 'Retry' end - describe "POST /:project/builds/:id/retry" do - before do - @build.cancel! - visit ci_project_build_path(@commit.project, @build) - click_link 'Retry' - end - - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } - end + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } end end From 0dd49560b95439a74eb25282127a3d8066b68e2a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 6 Oct 2015 21:42:32 -0400 Subject: [PATCH 215/543] Update omniauth-twitter to ~> 1.2.0 --- Gemfile | 24 ++++++++++++------------ Gemfile.lock | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile b/Gemfile index 4938cbf8b8..573fbedee6 100644 --- a/Gemfile +++ b/Gemfile @@ -22,20 +22,20 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem "devise", '~> 3.5.2' -gem "devise-async", '~> 0.9.0' -gem 'omniauth', "~> 1.2.2" +gem 'devise', '~> 3.5.2' +gem 'devise-async', '~> 0.9.0' +gem 'omniauth', '~> 1.2.2' +gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-github', '~> 1.1.1' +gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.5' -gem 'omniauth-twitter', '~> 1.0.1' -gem 'omniauth-github', '~> 1.1.1' -gem 'omniauth-shibboleth', '~> 1.1.1' -gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos -gem 'omniauth-gitlab', '~> 1.0.0' -gem 'omniauth-bitbucket', '~> 0.0.2' -gem 'omniauth-saml', '~> 1.4.0' -gem 'doorkeeper', '~> 2.1.3' +gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos +gem 'omniauth-saml', '~> 1.4.0' +gem 'omniauth-shibboleth', '~> 1.1.1' +gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' -gem "rack-oauth2", "~> 1.0.5" +gem 'doorkeeper', '~> 2.1.3' +gem 'rack-oauth2', '~> 1.0.5' # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1dd56cd9c8..8a0474a062 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -456,9 +456,9 @@ GEM ruby-saml (~> 1.0.0) omniauth-shibboleth (1.1.2) omniauth (>= 1.0.0) - omniauth-twitter (1.0.1) - multi_json (~> 1.3) - omniauth-oauth (~> 1.0) + omniauth-twitter (1.2.1) + json (~> 1.3) + omniauth-oauth (~> 1.1) omniauth_crowd (2.2.3) activesupport nokogiri (>= 1.4.4) @@ -874,7 +874,7 @@ DEPENDENCIES omniauth-kerberos (~> 0.2.0) omniauth-saml (~> 1.4.0) omniauth-shibboleth (~> 1.1.1) - omniauth-twitter (~> 1.0.1) + omniauth-twitter (~> 1.2.0) omniauth_crowd org-ruby (~> 0.9.12) paranoia (~> 2.0) From 25f77305c3cf66a0c0aa5f15e59be6afb7009be3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 6 Oct 2015 21:48:19 -0400 Subject: [PATCH 216/543] Update omniauth-kerberos to ~> 0.3.0 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 573fbedee6..59169b724c 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.5' -gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos +gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-shibboleth', '~> 1.1.1' gem 'omniauth-twitter', '~> 1.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8a0474a062..87c9f5aa08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -440,7 +440,7 @@ GEM omniauth-google-oauth2 (0.2.6) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-kerberos (0.2.0) + omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) omniauth-multipassword (0.4.2) @@ -871,7 +871,7 @@ DEPENDENCIES omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.2.5) - omniauth-kerberos (~> 0.2.0) + omniauth-kerberos (~> 0.3.0) omniauth-saml (~> 1.4.0) omniauth-shibboleth (~> 1.1.1) omniauth-twitter (~> 1.2.0) From 59abf498c67872da702ee35832859f6c07b37a8e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 6 Oct 2015 21:57:10 -0400 Subject: [PATCH 217/543] Loosen omniauth-google-oauth2 to ~> 0.2.0 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 59169b724c..98cd3bcd26 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,7 @@ gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' -gem 'omniauth-google-oauth2', '~> 0.2.5' +gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-shibboleth', '~> 1.1.1' From 33222b575603e6e24446ded8fa33ff36cfa25c0e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 6 Oct 2015 22:03:42 -0400 Subject: [PATCH 218/543] Update omniauth-shibboleth to ~> 1.2.0 --- Gemfile | 4 ++-- Gemfile.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 98cd3bcd26..bfcdeff16d 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries gem 'devise', '~> 3.5.2' gem 'devise-async', '~> 0.9.0' +gem 'doorkeeper', '~> 2.1.3' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-github', '~> 1.1.1' @@ -31,10 +32,9 @@ gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.4.0' -gem 'omniauth-shibboleth', '~> 1.1.1' +gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' -gem 'doorkeeper', '~> 2.1.3' gem 'rack-oauth2', '~> 1.0.5' # Two-factor authentication diff --git a/Gemfile.lock b/Gemfile.lock index 87c9f5aa08..1ae60c62d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -454,7 +454,7 @@ GEM omniauth-saml (1.4.1) omniauth (~> 1.1) ruby-saml (~> 1.0.0) - omniauth-shibboleth (1.1.2) + omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) omniauth-twitter (1.2.1) json (~> 1.3) @@ -870,10 +870,10 @@ DEPENDENCIES omniauth-bitbucket (~> 0.0.2) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) - omniauth-google-oauth2 (~> 0.2.5) + omniauth-google-oauth2 (~> 0.2.0) omniauth-kerberos (~> 0.3.0) omniauth-saml (~> 1.4.0) - omniauth-shibboleth (~> 1.1.1) + omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd org-ruby (~> 0.9.12) From 8dcc8e5db61b602baaf6390c6cbbaa28d49b3ee1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 10:26:40 +0200 Subject: [PATCH 219/543] Fix routing in CI mailer Signed-off-by: Dmitriy Zaporozhets --- app/models/project_services/ci/hip_chat_message.rb | 2 +- app/models/project_services/ci/slack_message.rb | 4 ++-- app/models/project_services/gitlab_ci_service.rb | 2 +- app/views/ci/notify/build_fail_email.html.haml | 2 +- app/views/ci/notify/build_fail_email.text.erb | 2 +- app/views/ci/notify/build_success_email.html.haml | 2 +- app/views/ci/notify/build_success_email.text.erb | 2 +- spec/models/project_services/gitlab_ci_service_spec.rb | 3 +-- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb index 0bf448d47f..cbf325cc52 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 a89c01517b..5ac8907ecd 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -28,7 +28,7 @@ module Ci next unless build.failed? fields << { title: build.name, - value: "Build <#{ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." + value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." } end @@ -45,7 +45,7 @@ module Ci def attachment_message out = "<#{ci_project_url(project)}|#{project_name}>: " - out << "Commit <#{ci_project_commits_url(project, commit.sha)}|\##{commit.id}> " + out << "Commit <#{ci_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 b63a75cf3a..4dcd16ede3 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_project_commits_url(project.gitlab_ci_project, sha) + ci_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 4ebdfa1b6c..69689a7502 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -16,4 +16,4 @@ Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} + Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb index 177827f9a3..6de5dc10f1 100644 --- a/app/views/ci/notify/build_fail_email.text.erb +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -6,4 +6,4 @@ Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> Message: <%= @build.commit.git_commit_message %> -Url: <%= ci_project_build_url(@build.project, @build) %> +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 7cc43300e8..4e3015a356 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/ci/notify/build_success_email.html.haml @@ -17,4 +17,4 @@ Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} + Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb index 4d55c39b0f..d0a43ae1c1 100644 --- a/app/views/ci/notify/build_success_email.text.erb +++ b/app/views/ci/notify/build_success_email.text.erb @@ -6,4 +6,4 @@ Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> Message: <%= @build.commit.git_commit_message %> -Url: <%= ci_project_build_url(@build.project, @build) %> +Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index c0b8a144c3..842089ebe0 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -39,8 +39,7 @@ describe GitlabCiService do end describe :build_page do - it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/commits/2ab7834c")} - it { expect(@service.build_page("issue#2", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/commits/issue%232")} + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/ci")} end describe "execute" do From f9aab3fb586614dc21fa799a5088dc53acd43215 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 11:43:38 +0200 Subject: [PATCH 220/543] Clarify Reply by email settings when using Postfix mail server --- config/mail_room.yml.example | 2 +- doc/incoming_email/README.md | 67 ++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example index 82e1a42058..ed4a5193a6 100644 --- a/config/mail_room.yml.example +++ b/config/mail_room.yml.example @@ -9,7 +9,7 @@ # # Whether the IMAP server uses StartTLS # :start_tls: false # # Email account username. Usually the full email address. - # :email: "replies@gitlab.example.com" + # :email: "gitlab-incoming@gmail.com" # # Email account password # :password: "password" # # The name of the mailbox where incoming mail will end up. Usually "inbox". diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 316746ab54..8726742347 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -16,24 +16,35 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ## Set it up -In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. - ### 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: ```ruby + # Postfix mail server, assumes mailbox incoming@gitlab.example.com + gitlab_rails['incoming_email_enabled'] = true + 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". + ``` + + ```ruby + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com gitlab_rails['incoming_email_enabled'] = true 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'] = "password" # Email account password + 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". ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + 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`. 1. Reconfigure GitLab for the changes to take effect: @@ -64,12 +75,20 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. ``` ```yaml + # Postfix mail server, assumes mailbox incoming@gitlab.example.com + incoming_email: + enabled: true + address: "incoming+%{key}@gitlab.example.com" + ``` + + ```yaml + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true address: "gitlab-incoming+%{key}@gmail.com" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + 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`: @@ -84,6 +103,40 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. ``` ```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 + ``` + + ```yaml + # Gmail / Google Apps :mailboxes: - # IMAP server host @@ -143,6 +196,7 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. 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: ```yaml + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true address: "gitlab-incoming+%{key}@gmail.com" @@ -159,6 +213,7 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. 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 @@ -208,4 +263,4 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. bundle exec rake gitlab:incoming_email:check RAILS_ENV=development ``` -8. Reply by email should now be working. \ No newline at end of file +8. Reply by email should now be working. From 0ab6ca93aadaf08f65f36e7fbdb5b837bac6e160 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 16 Sep 2015 22:45:22 -0700 Subject: [PATCH 221/543] Add directory feature button Change "+" icon under "Files" section to have three options: * Create file * Upload file * New directory Upload file is no longer accessible from the "Create file" page. Users can now select a target branch in upload file as well. Closes #2799: Fixes a bug where file modes were overwritten after a commit Closes https://github.com/gitlabhq/gitlabhq/issues/8253: Existing files can no longer be overwritten in the "Create file" section. Closes #2557 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 +- .../blob/blob_file_dropzone.js.coffee | 1 + app/controllers/projects/blob_controller.rb | 8 +-- app/controllers/projects/tree_controller.rb | 37 +++++++++++ app/models/repository.rb | 29 ++++++--- app/services/files/create_dir_service.rb | 9 +++ app/services/files/create_service.rb | 2 +- app/services/files/update_service.rb | 2 +- app/services/merge_requests/merge_service.rb | 2 +- app/views/projects/blob/_new_dir.html.haml | 25 ++++++++ app/views/projects/blob/_upload.html.haml | 9 ++- app/views/projects/blob/new.html.haml | 7 +-- app/views/projects/tree/_tree.html.haml | 26 ++++++-- config/routes.rb | 9 +++ features/project/source/browse_files.feature | 33 +++++++--- features/steps/project/source/browse_files.rb | 63 +++++++++++++++---- .../projects/tree_controller_spec.rb | 36 +++++++++++ 19 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 app/services/files/create_dir_service.rb create mode 100644 app/views/projects/blob/_new_dir.html.haml diff --git a/CHANGELOG b/CHANGELOG index 388fa2f896..9d55622dd5 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) + - Add support for creating directories from Files page (Stan Hu) - 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) - Add user preference to view activities as default dashboard (Stan Hu) diff --git a/Gemfile b/Gemfile index 4938cbf8b8..5572fa0b6c 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.18' # 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..5eaf380842 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.18) 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.18) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee index 3ab3ba6675..5b604adbbb 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -47,6 +47,7 @@ class @BlobFileDropzone return this.on 'sending', (file, xhr, formData) -> + formData.append('new_branch', form.find('#new_branch').val()) formData.append('commit_message', form.find('#commit_message').val()) return diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8776721d24..ae9b138446 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -8,7 +8,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:destroy] + before_action :authorize_push_code!, only: [:destroy, :create] before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] @@ -25,7 +25,7 @@ class Projects::BlobController < Projects::ApplicationController result = Files::CreateService.new(@project, current_user, @commit_params).execute if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" + flash[:notice] = "The changes have been successfully committed" respond_to do |format| format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } } @@ -34,7 +34,7 @@ class Projects::BlobController < Projects::ApplicationController flash[:alert] = result[:message] respond_to do |format| format.html { render :new } - format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } + format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } } end end end @@ -154,7 +154,7 @@ class Projects::BlobController < Projects::ApplicationController def editor_variables @current_branch = @ref - @target_branch = (sanitized_new_branch_name || @ref) + @target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref @file_path = if action_name.to_s == 'create' diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 92e4bc16d9..7eaff1d61e 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,10 +1,13 @@ # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath + include ActionView::Helpers::SanitizeHelper before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars + before_action :assign_dir_vars, only: [:create_dir] before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create_dir] def show return not_found! unless @repository.commit(@ref) @@ -26,4 +29,38 @@ class Projects::TreeController < Projects::ApplicationController format.js { no_cache_headers } end end + + def create_dir + return not_found! unless @commit_params.values.all? + + begin + result = Files::CreateDirService.new(@project, current_user, @commit_params).execute + message = result[:message] + rescue => e + message = e.to_s + end + + if result && result[:status] == :success + flash[:notice] = "The directory has been successfully created" + respond_to do |format| + format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } + end + else + flash[:alert] = message + respond_to do |format| + format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) } + end + end + end + + def assign_dir_vars + @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref + @dir_name = File.join(@path, params[:dir_name]) + @commit_params = { + file_path: @dir_name, + current_branch: @ref, + target_branch: @new_branch, + commit_message: params[:commit_message], + } + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 2c5ab62d22..8b51602bc2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -373,11 +373,25 @@ class Repository @root_ref ||= raw_repository.root_ref end - def commit_file(user, path, content, message, branch) + def commit_dir(user, path, message, branch) commit_with_hooks(user, branch) do |ref| - path[0] = '' if path[0] == '/' + committer = user_to_committer(user) + options = {} + options[:committer] = committer + options[:author] = committer - committer = user_to_comitter(user) + options[:commit] = { + message: message, + branch: ref, + } + + raw_repository.mkdir(path, options) + end + end + + def commit_file(user, path, content, message, branch, update) + commit_with_hooks(user, branch) do |ref| + committer = user_to_committer(user) options = {} options[:committer] = committer options[:author] = committer @@ -388,7 +402,8 @@ class Repository options[:file] = { content: content, - path: path + path: path, + update: update } Gitlab::Git::Blob.commit(raw_repository, options) @@ -397,9 +412,7 @@ class Repository def remove_file(user, path, message, branch) commit_with_hooks(user, branch) do |ref| - path[0] = '' if path[0] == '/' - - committer = user_to_comitter(user) + committer = user_to_committer(user) options = {} options[:committer] = committer options[:author] = committer @@ -416,7 +429,7 @@ class Repository end end - def user_to_comitter(user) + def user_to_committer(user) { email: user.email, name: user.name, diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb new file mode 100644 index 0000000000..71272fb570 --- /dev/null +++ b/app/services/files/create_dir_service.rb @@ -0,0 +1,9 @@ +require_relative "base_service" + +module Files + class CreateDirService < Files::BaseService + def commit + repository.commit_dir(current_user, @file_path, @commit_message, @target_branch) + end + end +end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index ffbb599327..c8e3a910bb 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class CreateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false) end def validate diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a20903c6f0..1960dc7d94 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class UpdateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, true) end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fcc0f2a6a8..7963af127e 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -29,7 +29,7 @@ module MergeRequests private def commit - committer = repository.user_to_comitter(current_user) + committer = repository.user_to_committer(current_user) options = { message: commit_message, diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml new file mode 100644 index 0000000000..cb1567a2e6 --- /dev/null +++ b/app/views/projects/blob/_new_dir.html.haml @@ -0,0 +1,25 @@ +#modal-create-new-dir.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title Create New Directory + .modal-body + = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do + .form-group + = label_tag :dir_name, 'Directory Name', class: 'control-label' + .col-sm-10 + = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' + = render 'shared/commit_message_container', params: params, placeholder: '' + - unless @project.empty_repo? + .form-group + = label_tag :branch_name, 'Branch', class: 'control-label' + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + .form-group + .col-sm-offset-2.col-sm-10 + = submit_tag "Create directory", class: 'btn btn-primary btn-create' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + +:coffeescript + 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 1a1df12770..e27f170752 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -4,9 +4,6 @@ .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title #{title} - %p.light - From branch - %strong= @ref .modal-body = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do .dropzone @@ -18,6 +15,12 @@ .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} = render 'shared/commit_message_container', params: params, placeholder: placeholder + - unless @project.empty_repo? + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" .form-group .col-sm-offset-2.col-sm-10 = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1950586b11..d7987e24ef 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -2,12 +2,7 @@ = render "header_title" .gray-content-block.top-block - Create a new file or - = link_to 'upload', '#modal-upload-blob', - { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} - an existing one - -= 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 + Create a new file .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 367a87927d..457f8a4a58 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -8,11 +8,25 @@ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' - - if current_user && can_push_branch?(@project, @ref) + - if allowed_tree_edit? %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do - %small - %i.fa.fa-plus + %span.dropdown + %a.dropdown-toggle.btn.btn-xs.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.prepend-top-20 %table#tree-slider{class: "table_#{@hex_path} tree-table" } @@ -46,6 +60,10 @@ %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' + :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { diff --git a/config/routes.rb b/config/routes.rb index ccce40589e..035b996dd7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -463,6 +463,15 @@ Gitlab::Application.routes.draw do ) end + scope do + post( + '/create_dir/*id', + to: 'tree#create_dir', + constraints: { id: /.+/ }, + as: 'create_dir' + ) + end + scope do get( '/blame/*id', diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 58574166ef..377c5e1a9a 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -21,12 +21,12 @@ Feature: Project Source Browse Files Then I should see raw file content Scenario: I can create file - Given I click on "new file" link in repo + Given I click on "New file" link in repo Then I can see new file page @javascript Scenario: I can create and commit file - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I edit code And I fill the new file name And I fill the commit message @@ -36,14 +36,13 @@ Feature: Project Source Browse Files @javascript Scenario: I can upload file and commit - Given I click on "new file" link in repo - Then I can see new file page - And I can see "upload an existing one" - And I click on "upload" + Given I click on "Upload file" link in repo And I upload a new text file And I fill the upload file commit message + And I fill the new branch name And I click on "Upload file" Then I can see the new text file + And I am redirected to the uploaded file on new branch And I can see the new commit message @javascript @@ -59,7 +58,7 @@ Feature: Project Source Browse Files @javascript Scenario: I can create and commit file and specify new branch - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I edit code And I fill the new file name And I fill the commit message @@ -83,7 +82,7 @@ Feature: Project Source Browse Files @javascript Scenario: If I enter an illegal file name I see an error message - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I fill the new file name with an illegal name And I edit code And I fill the commit message @@ -138,6 +137,24 @@ Feature: Project Source Browse Files Then I am on the ".gitignore" edit file page And I see a commit error message + @javascript + Scenario: I can create directory in repo + When I click on "New directory" link in repo + And I fill the new directory name + And I fill the commit message + And I fill the new branch name + And I click on "Create directory" + Then I am redirected to the new directory + + @javascript + Scenario: I attempt to create an existing directory + When I click on "New directory" link in repo + And I fill an existing directory name + And I fill the commit message + And I click on "Create directory" + Then I see "Unable to create directory" + And I am redirected to the root directory + @javascript Scenario: I can see editing preview Given I click on ".gitignore" file in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index a1a49dd58a..cb100ca0f5 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -71,7 +71,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new branch name' do - fill_in :new_branch, with: 'new_branch_name' + fill_in :new_branch, with: 'new_branch_name', visible: true end step 'I fill the new file name with an illegal name' do @@ -90,6 +90,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_button 'Commit Changes' end + step 'I click on "Create directory"' do + click_button 'Create directory' + end + step 'I click on "Remove"' do click_button 'Remove' end @@ -110,8 +114,27 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_css '.line_holder.new' end - step 'I click on "new file" link in repo' do - click_link 'new-file-link' + step 'I click on "New file" link in repo' do + find('.add-to-tree').click + click_link 'Create file' + end + + step 'I click on "Upload file" link in repo' do + find('.add-to-tree').click + click_link 'Upload file' + end + + step 'I click on "New directory" link in repo' do + find('.add-to-tree').click + click_link 'New directory' + end + + step 'I fill the new directory name' do + fill_in :dir_name, with: new_dir_name + end + + step 'I fill an existing directory name' do + fill_in :dir_name, with: 'files' end step 'I can see new file page' do @@ -119,14 +142,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content "Commit message" end - step 'I can see "upload an existing one"' do - expect(page).to have_content "upload an existing one" - end - - step 'I click on "upload"' do - click_link 'upload' - end - step 'I click on "Upload file"' do click_button 'Upload file' end @@ -228,10 +243,30 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps @project.namespace, @project, 'new_branch_name/' + new_file_name)) end + step 'I am redirected to the uploaded file on new branch' do + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, + 'new_branch_name/' + File.basename(test_text_file))) + end + + step 'I am redirected to the new directory' do + expect(current_path).to eq(namespace_project_tree_path( + @project.namespace, @project, 'new_branch_name/' + new_dir_name)) + end + + step 'I am redirected to the root directory' do + expect(current_path).to eq(namespace_project_tree_path( + @project.namespace, @project, 'master/')) + end + step "I don't see the permalink link" do expect(page).not_to have_link('permalink') end + step 'I see "Unable to create directory"' do + expect(page).to have_content('Directory already exists') + end + step 'I see a commit error message' do expect(page).to have_content('Your changes could not be committed') end @@ -287,6 +322,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps 'not_a_file.md' end + # Constant value that is a valid directory and + # not a directory present at root of the seed repository. + def new_dir_name + 'new_dir/subdir' + end + def drop_in_dropzone(file_path) # Generate a fake input selector page.execute_script <<-JS diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index 5391585635..a474574c6e 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -88,4 +88,40 @@ describe Projects::TreeController do end end end + + describe '#create_dir' do + render_views + + before do + post(:create_dir, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master', + dir_name: path, + new_branch: target_branch, + commit_message: 'Test commit message') + end + + context 'successful creation' do + let(:path) { 'files/new_dir'} + let(:target_branch) { 'master-test'} + + it 'redirects to the new directory' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}") + expect(flash[:notice]).to eq('The directory has been successfully created') + end + end + + context 'unsuccessful creation' do + let(:path) { 'README.md' } + let(:target_branch) { 'master'} + + it 'does not allow overwriting of existing files' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/blob/master") + expect(flash[:alert]).to eq('Directory already exists as a file') + end + end + end end From 6f6097713ca593c7a7177df0f06420b124d478aa Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 14:26:07 +0200 Subject: [PATCH 222/543] Move status badge helper to CI project settings for now Signed-off-by: Dmitriy Zaporozhets --- app/views/ci/projects/show.html.haml | 16 ---------------- app/views/projects/ci_settings/_form.html.haml | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml index 888b1ea41d..f5244696c5 100644 --- a/app/views/ci/projects/show.html.haml +++ b/app/views/ci/projects/show.html.haml @@ -19,22 +19,6 @@ %li.pull-right = link_to 'Go to project', project_path(gl_project), class: 'btn btn-sm' -- if @ref - %p - Paste build status image for #{@ref} with next link - = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do - Status Badge - .badge-codes-block.bs-callout.bs-callout-info.hide - %p - Status badge for - %span.label.label-info #{@ref} - branch - %div - %label Markdown: - = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control' - %label Html: - = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control' - diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml index 9f891f557a..3412555020 100644 --- a/app/views/projects/ci_settings/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -8,6 +8,22 @@ Edit your #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} +- if @repository + %p + Paste build status image for #{@repository.root_ref} with next link + = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do + Status Badge + .badge-codes-block.bs-callout.bs-callout-info.hide + %p + Status badge for + %span.label.label-info #{@ref} + branch + %div + %label Markdown: + = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' + %label Html: + = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' + = nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - if @ci_project.errors.any? #error_explanation From c0b79a751c8cbf676900956b14b70edd5dca5010 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 14:35:54 +0200 Subject: [PATCH 223/543] Move no runners alert to project ci settings Signed-off-by: Dmitriy Zaporozhets --- app/views/ci/projects/_info.html.haml | 2 -- .../projects => projects/ci_settings}/_no_runners.html.haml | 2 +- app/views/projects/ci_settings/edit.html.haml | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) rename app/views/{ci/projects => projects/ci_settings}/_no_runners.html.haml (87%) diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml index 1888e1bde9..e69de29bb2 100644 --- a/app/views/ci/projects/_info.html.haml +++ b/app/views/ci/projects/_info.html.haml @@ -1,2 +0,0 @@ -- if no_runners_for_project?(@project) - = render 'no_runners' diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml similarity index 87% rename from app/views/ci/projects/_no_runners.html.haml rename to app/views/projects/ci_settings/_no_runners.html.haml index c0a296fb17..33038c5297 100644 --- a/app/views/ci/projects/_no_runners.html.haml +++ b/app/views/projects/ci_settings/_no_runners.html.haml @@ -4,5 +4,5 @@ %br You can add Specific runner for this project on Runners page - - if current_user.is_admin + - if current_user.admin or add Shared runner for whole application in admin are. diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml index e9040fe433..eedf484bf0 100644 --- a/app/views/projects/ci_settings/edit.html.haml +++ b/app/views/projects/ci_settings/edit.html.haml @@ -6,6 +6,9 @@ yaml file which is based on your old jobs. Put this file to the root of your project and name it .gitlab-ci.yml +- if no_runners_for_project?(@ci_project) + = render 'no_runners' + = render 'form' - if @ci_project.generated_yaml_config From e52de6771f6bf9629fc85213b94ec60721354e80 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 14:43:58 +0200 Subject: [PATCH 224/543] Remove Continuous Integration from project menu Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/projects_controller.rb | 23 ++-------- app/views/ci/projects/_info.html.haml | 0 app/views/ci/projects/show.html.haml | 44 ------------------- app/views/layouts/ci/_nav_project.html.haml | 6 --- app/views/layouts/nav/_project.html.haml | 7 --- .../layouts/nav/_project_settings.html.haml | 10 +++++ 6 files changed, 13 insertions(+), 77 deletions(-) delete mode 100644 app/views/ci/projects/_info.html.haml delete mode 100644 app/views/ci/projects/show.html.haml diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index adb913a783..5484ae643a 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,27 +1,10 @@ module Ci class ProjectsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:build, :badge, :show] - before_action :authenticate_public_page!, only: :show - before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml] - before_action :authorize_access_project!, except: [:build, :badge, :show, :new] + before_action :authenticate_user!, except: [:build, :badge] + before_action :authorize_access_project!, except: [:badge] before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] - before_action :authenticate_token!, only: [:build] before_action :no_cache, only: [:badge] - protect_from_forgery except: :build - - layout 'ci/project', except: [:index] - - def show - @ref = params[:ref] - - @commits = @project.commits.reverse_order - if @ref - # unscope is required, because of default_scope defined in Ci::Build - builds = @project.builds.unscope(:select, :order).where(ref: @ref).select(:commit_id).distinct - @commits = @commits.where(id: builds) - end - @commits = @commits.page(params[:page]).per(20) - end + protect_from_forgery # Project status badge # Image with build status for sha or ref diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml deleted file mode 100644 index f5244696c5..0000000000 --- a/app/views/ci/projects/show.html.haml +++ /dev/null @@ -1,44 +0,0 @@ -= render 'ci/shared/guide' unless @project.setup_finished? - -- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners? - .alert.alert-danger - Builds for this project wont be served unless you configure runners on - = link_to "Runners page", runners_path(@project.gl_project) - -%ul.nav.nav-tabs.append-bottom-20 - %li{class: ref_tab_class} - = link_to 'All commits', ci_project_path(@project) - - @project.tracked_refs.each do |ref| - %li{class: ref_tab_class(ref)} - = link_to ref, ci_project_path(@project, ref: ref) - - - if @ref && !@project.tracked_refs.include?(@ref) - %li{class: 'active'} - = link_to @ref, ci_project_path(@project, ref: @ref) - - %li.pull-right - = link_to 'Go to project', project_path(gl_project), class: 'btn btn-sm' - - - - -%table.table.builds - %thead - %tr - %th Status - %th Commit - %th Message - %th Branch - %th Total duration - %th Finished at - - if @project.coverage_enabled? - %th Coverage - - = render @commits - -= paginate @commits - -- if @commits.empty? - .bs-callout - %h4 No commits yet - diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 545abc23d9..db631f2465 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -5,12 +5,6 @@ %span Back to project %li.separate-item - = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do - = link_to ci_project_path(@project) do - = icon('list-alt fw') - %span - Commits - %span.count= @project.commits.count = nav_link path: ['services#index', 'services#edit'] do = link_to ci_project_services_path(@project) do = icon('share fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a218ec7486..8ce46d4865 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -76,13 +76,6 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count - - if @project.gitlab_ci? - = nav_link(controller: [:ci, :project]) do - = link_to ci_project_path(@project.gitlab_ci_project), title: 'Continuous Integration', data: {placement: 'right'} do - = icon('building fw') - %span - Continuous Integration - - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', 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 9279a84662..f618a18870 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -60,3 +60,13 @@ = icon('building fw') %span CI Settings + = nav_link path: ['ci/services#index', 'ci/services#edit'] do + = link_to ci_project_services_path(@project.gitlab_ci_project) do + = 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 From 7d220c1e7936b39affe1d819b7731c608795c7e9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 14:56:21 +0200 Subject: [PATCH 225/543] Remove test for removed page and add menu highlight for build page Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_project.html.haml | 2 +- spec/features/ci/projects_spec.rb | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 spec/features/ci/projects_spec.rb diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8ce46d4865..e4c285d802 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 builds)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb deleted file mode 100644 index c633acf85f..0000000000 --- a/spec/features/ci/projects_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe "Projects" do - let(:user) { create(:user) } - - before do - login_as(user) - @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" - @project.gl_project.team << [user, :master] - end - - describe "GET /ci/projects/:id" do - before do - visit ci_project_path(@project) - end - - it { expect(page).to have_content @project.name } - it { expect(page).to have_content 'All commits' } - end -end From 3fa2cb93353720e1b70e01ec9e664ebf54d1fc29 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 14:59:37 +0200 Subject: [PATCH 226/543] Remove unused JS Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/ci/Chart.min.js | 39 -------------------- app/assets/javascripts/ci/projects.js.coffee | 3 -- 2 files changed, 42 deletions(-) delete mode 100644 app/assets/javascripts/ci/Chart.min.js diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js deleted file mode 100644 index ab63588108..0000000000 --- a/app/assets/javascripts/ci/Chart.min.js +++ /dev/null @@ -1,39 +0,0 @@ -var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= -Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& -isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? -b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? -0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== -a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* -(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* -a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, -scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", -animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", -scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, -c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, -onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, -pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", -scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); -d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; -h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< -h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= -Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0 $('.badge-codes-block').toggleClass("hide") return false - -$(document).on 'click', '.sync-now', -> - $(this).find('i').addClass('fa-spin') From 12626f029dc4ab345f8afa1fd42a3c04723ec55e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 15:17:43 +0200 Subject: [PATCH 227/543] 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 1e06cabf4a8fa4d4c7acb9898682a5b4b41a9f58 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 15:24:32 +0200 Subject: [PATCH 228/543] Remove Ci::Commit and Ci::Build controllers Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/builds_controller.rb | 52 ------------------- app/controllers/ci/commits_controller.rb | 32 ------------ app/controllers/projects/builds_controller.rb | 30 +++++++++++ app/controllers/projects/commit_controller.rb | 8 +++ app/views/projects/builds/_build.html.haml | 4 +- app/views/projects/builds/show.html.haml | 4 +- app/views/projects/commit/ci.html.haml | 2 +- config/routes.rb | 24 +++------ spec/features/builds_spec.rb | 21 +++++++- spec/features/ci/builds_spec.rb | 31 ----------- 10 files changed, 71 insertions(+), 137 deletions(-) delete mode 100644 app/controllers/ci/builds_controller.rb delete mode 100644 app/controllers/ci/commits_controller.rb delete mode 100644 spec/features/ci/builds_spec.rb diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb deleted file mode 100644 index b0b8b62fce..0000000000 --- a/app/controllers/ci/builds_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Ci - class BuildsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:status] - before_action :project - before_action :authorize_access_project!, except: [:status] - before_action :authorize_manage_project!, except: [:status, :retry, :cancel] - before_action :authorize_manage_builds!, only: [:retry, :cancel] - before_action :build - - def retry - if @build.commands.blank? - 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 - end - - def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) - end - - def cancel - @build.cancel - - redirect_to build_path(@build) - end - - protected - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def build - @build ||= project.builds.unscoped.find_by!(id: params[:id]) - end - - def commit_by_sha - @project.commits.find_by(sha: params[:id]) - end - - def build_path(build) - namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) - end - end -end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb deleted file mode 100644 index 7e6705c970..0000000000 --- a/app/controllers/ci/commits_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Ci - class CommitsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:status, :show] - before_action :authenticate_public_page!, only: :show - before_action :project - before_action :authorize_access_project!, except: [:status, :show, :cancel] - before_action :authorize_manage_builds!, only: [:cancel] - - def status - commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) - render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) - rescue ActiveRecord::RecordNotFound - render json: { status: "not_found" } - end - - def cancel - commit.builds.running_or_pending.each(&:cancel) - - redirect_to namespace_project_commit_path(commit.gl_project.namespace, commit.gl_project, commit.sha) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - - def commit - @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) - end - end -end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 76c7f31f61..4e4ac6689d 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -2,6 +2,8 @@ class Projects::BuildsController < Projects::ApplicationController before_action :ci_project before_action :build + before_action :authorize_admin_project!, except: [:show, :status] + layout "project" def show @@ -17,9 +19,37 @@ class Projects::BuildsController < Projects::ApplicationController end end + def retry + if @build.commands.blank? + 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 + end + + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + + def cancel + @build.cancel + + redirect_to build_path(@build) + end + private def build @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) end + + def build_path(build) + namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) + end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 1938c63c10..c08a90bddf 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -38,6 +38,14 @@ class Projects::CommitController < Projects::ApplicationController @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) + + redirect_to namespace_project_commit_path(project.namespace, project, commit.sha) + end + + def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml index 21c543b38d..65fd9413b6 100644 --- a/app/views/projects/builds/_build.html.haml +++ b/app/views/projects/builds/_build.html.haml @@ -43,8 +43,8 @@ - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) .pull-right - if build.active? - = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do + = link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), title: 'Cancel build' do %i.fa.fa-remove.cred - elsif build.commands.present? - = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do + = link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), method: :post, title: 'Retry build' do %i.fa.fa-repeat diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 93cd4dcfd9..b561078e8c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -72,9 +72,9 @@ - if current_user && can?(current_user, :manage_builds, @project) .pull-right - if @build.active? - = link_to "Cancel", cancel_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-danger' + = 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_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-primary', method: :post + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post - if @build.duration %p diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index f4382e8804..26ab38445c 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -6,7 +6,7 @@ - if @ci_project && current_user && can?(current_user, :manage_builds, @project) .pull-right - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel", cancel_ci_project_commits_path(@ci_project, @ci_commit), class: 'btn btn-sm btn-danger' + = link_to "Cancel", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-sm btn-danger' - if @ci_commit.yaml_errors.present? diff --git a/config/routes.rb b/config/routes.rb index ccce40589e..4fb779e297 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,21 +28,6 @@ Gitlab::Application.routes.draw do end end - resources :commits, only: [] do - member do - get :status - get :cancel - end - end - - resources :builds, only: [] do - member do - get :cancel - get :status - post :retry - end - end - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -486,6 +471,7 @@ Gitlab::Application.routes.draw do member do get :branches get :ci + post :cancel_builds end end @@ -590,7 +576,13 @@ Gitlab::Application.routes.draw do end end - resources :builds, only: [:show] + resources :builds, only: [:show] do + member do + get :cancel + get :status + post :retry + end + end resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index d0d60491b6..924047a0d8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe "Builds" do - before do login_as(:user) @commit = FactoryGirl.create :ci_commit @@ -19,4 +18,24 @@ describe "Builds" do it { expect(page).to have_content @commit.git_commit_message } it { expect(page).to have_content @commit.git_author_name } end + + describe "GET /:project/builds/:id/cancel" do + before do + @build.run! + visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } + end + + describe "POST /:project/builds/:id/retry" do + before do + visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Retry' + end + + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } + end end diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb deleted file mode 100644 index aa0df59c04..0000000000 --- a/spec/features/ci/builds_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe "Builds" do - before do - login_as(:user) - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - @gl_project = @commit.project.gl_project - @gl_project.team << [@user, :master] - end - - describe "GET /:project/builds/:id/cancel" do - before do - @build.run! - visit cancel_ci_project_build_path(@commit.project, @build) - end - - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } - end - - describe "POST /:project/builds/:id/retry" do - before do - visit cancel_ci_project_build_path(@commit.project, @build) - click_link 'Retry' - end - - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } - end -end From 75bb1087df785e75fb343a9e8721e8d636f7ce81 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 6 Oct 2015 21:08:27 -0700 Subject: [PATCH 229/543] Allow removing of project without confirmation when JavaScript is disabled Closes #2485 --- CHANGELOG | 1 + app/views/projects/edit.html.haml | 4 ++-- spec/features/projects_spec.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d55622dd5..77e5ad2681 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) - Add support for creating directories from Files page (Stan Hu) + - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) - 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) - Add user preference to view activities as default dashboard (Stan Hu) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 90dce73999..1882a82fba 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -193,13 +193,13 @@ .panel.panel-default.panel.panel-danger .panel-heading Remove project .panel-body - = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, html: { class: 'form-horizontal'}) do + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do %p Removing the project will delete its repository and all related resources including issues, merge requests etc. %br %strong Removed projects cannot be restored! - = link_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + = 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 diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index a362c54b9a..aac93b17a3 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -50,7 +50,7 @@ feature 'Project', feature: true do end def remove_project - click_link "Remove project" + click_button "Remove project" fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end From a30b68fe1ded96c5aafe4348d1afec269354c469 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 16:20:31 +0200 Subject: [PATCH 230/543] Move CI services to project settings area Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + app/controllers/ci/services_controller.rb | 59 ------------------- .../projects/ci_services_controller.rb | 49 +++++++++++++++ app/views/layouts/ci/_nav_project.html.haml | 5 -- .../layouts/nav/_project_settings.html.haml | 4 +- .../ci_services}/_form.html.haml | 7 +-- .../ci_services}/edit.html.haml | 0 .../ci_services}/index.html.haml | 2 +- config/routes.rb | 12 ++-- .../controllers/ci/commits_controller_spec.rb | 23 -------- 10 files changed, 61 insertions(+), 101 deletions(-) delete mode 100644 app/controllers/ci/services_controller.rb create mode 100644 app/controllers/projects/ci_services_controller.rb rename app/views/{ci/services => projects/ci_services}/_form.html.haml (80%) rename app/views/{ci/services => projects/ci_services}/edit.html.haml (100%) rename app/views/{ci/services => projects/ci_services}/index.html.haml (81%) delete mode 100644 spec/controllers/ci/commits_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 388fa2f896..5ea1deea68 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 8.1.0 (unreleased) - Add user preference to change layout width (Peter Göbel) - Use commit status in merge request widget as preffered source of CI status - Integrate CI commit and build pages into project pages + - Move CI services page to project settings area 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/ci/services_controller.rb b/app/controllers/ci/services_controller.rb deleted file mode 100644 index 52c96a34ce..0000000000 --- a/app/controllers/ci/services_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Ci - class ServicesController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_access_project! - before_action :authorize_manage_project! - before_action :service, only: [:edit, :update, :test] - - respond_to :html - - layout 'ci/project' - - def index - @project.build_missing_services - @services = @project.services.reload - end - - def edit - end - - def update - if @service.update_attributes(service_params) - redirect_to edit_ci_project_service_path(@project, @service.to_param) - else - render 'edit' - end - end - - def test - last_build = @project.builds.last - - if @service.execute(last_build) - message = { notice: 'We successfully tested the service' } - else - message = { alert: 'We tried to test the service but error occurred' } - end - - redirect_to :back, message - end - - private - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def service - @service ||= @project.services.find { |service| service.to_param == params[:id] } - end - - def service_params - params.require(:service).permit( - :type, :active, :webhook, :notify_only_broken_builds, - :email_recipients, :email_only_broken_builds, :email_add_pusher, - :hipchat_token, :hipchat_room, :hipchat_server - ) - end - end -end diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb new file mode 100644 index 0000000000..6d2756eba3 --- /dev/null +++ b/app/controllers/projects/ci_services_controller.rb @@ -0,0 +1,49 @@ +class Projects::CiServicesController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout "project_settings" + + def index + @ci_project.build_missing_services + @services = @ci_project.services.reload + end + + def edit + service + end + + def update + if @service.update_attributes(service_params) + redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param) + else + render 'edit' + end + end + + def test + last_build = @project.builds.last + + if @service.execute(last_build) + message = { notice: 'We successfully tested the service' } + else + message = { alert: 'We tried to test the service but error occurred' } + end + + redirect_to :back, message + end + + private + + def service + @service ||= @ci_project.services.find { |service| service.to_param == params[:id] } + end + + def service_params + params.require(:service).permit( + :type, :active, :webhook, :notify_only_broken_builds, + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server + ) + end +end diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index db631f2465..f094edbfa8 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -5,11 +5,6 @@ %span Back to project %li.separate-item - = nav_link path: ['services#index', 'services#edit'] do - = link_to ci_project_services_path(@project) do - = icon('share fw') - %span - Services = nav_link path: 'events#index' do = link_to ci_project_events_path(@project) do = icon('book fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index f618a18870..954dbe5d2b 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -60,8 +60,8 @@ = icon('building fw') %span CI Settings - = nav_link path: ['ci/services#index', 'ci/services#edit'] do - = link_to ci_project_services_path(@project.gitlab_ci_project) do + = nav_link controller: 'ci_services' do + = link_to namespace_project_ci_services_path(@project.namespace, @project) do = icon('share fw') %span CI Services diff --git a/app/views/ci/services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml similarity index 80% rename from app/views/ci/services/_form.html.haml rename to app/views/projects/ci_services/_form.html.haml index 9110aaa052..397832e56d 100644 --- a/app/views/ci/services/_form.html.haml +++ b/app/views/projects/ci_services/_form.html.haml @@ -4,13 +4,10 @@ %p= @service.description -.back-link - = link_to ci_project_services_path(@project) do - ← to services %hr -= form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| += form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - if @service.errors.any? .alert.alert-danger %ul @@ -54,4 +51,4 @@ = f.submit 'Save', class: 'btn btn-save'   - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn' + = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn' diff --git a/app/views/ci/services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml similarity index 100% rename from app/views/ci/services/edit.html.haml rename to app/views/projects/ci_services/edit.html.haml diff --git a/app/views/ci/services/index.html.haml b/app/views/projects/ci_services/index.html.haml similarity index 81% rename from app/views/ci/services/index.html.haml rename to app/views/projects/ci_services/index.html.haml index 37e5723b54..c78b21884a 100644 --- a/app/views/ci/services/index.html.haml +++ b/app/views/projects/ci_services/index.html.haml @@ -13,7 +13,7 @@ %td = boolean_to_icon service.activated? %td - = link_to edit_ci_project_service_path(@project, service.to_param) do + = link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do %strong= service.title %td = service.description diff --git a/config/routes.rb b/config/routes.rb index 4fb779e297..e91b09de9b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,12 +22,6 @@ Gitlab::Application.routes.draw do get :dumped_yaml end - resources :services, only: [:index, :edit, :update] do - member do - get :test - end - end - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -576,6 +570,12 @@ Gitlab::Application.routes.draw do end end + resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end + end + resources :builds, only: [:show] do member do get :cancel diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb deleted file mode 100644 index cc39ce7687..0000000000 --- a/spec/controllers/ci/commits_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "spec_helper" - -describe Ci::CommitsController do - describe "GET /status" do - it "returns status of commit" do - commit = FactoryGirl.create :ci_commit - get :status, id: commit.sha, ref_id: commit.ref, project_id: commit.project.id - - expect(response).to be_success - expect(response.code).to eq('200') - JSON.parse(response.body)["status"] == "pending" - end - - it "returns not_found status" do - commit = FactoryGirl.create :ci_commit - get :status, id: commit.sha, ref_id: "deploy", project_id: commit.project.id - - expect(response).to be_success - expect(response.code).to eq('200') - JSON.parse(response.body)["status"] == "not_found" - end - end -end From dfbbc80611fbdafe6f5ed809f98fc63987d104a6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 6 Oct 2015 09:57:13 -0700 Subject: [PATCH 231/543] Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters Closes #2619 Closes https://github.com/gitlabhq/gitlabhq/issues/9631 --- CHANGELOG | 1 + app/finders/issuable_finder.rb | 28 +++++++++++++++------ app/helpers/labels_helper.rb | 4 ++- app/models/label.rb | 3 +++ app/views/shared/issuable/_filter.html.haml | 4 +-- spec/finders/issues_finder_spec.rb | 20 +++++++++++++++ spec/helpers/labels_helper_spec.rb | 5 ++++ 7 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d55622dd5..43fc8b1d3b 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) - Add support for creating directories from Files page (Stan Hu) + - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) - 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) - Add user preference to view activities as default dashboard (Stan Hu) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 6aa16673d6..97c7e74c29 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -72,11 +72,15 @@ class IssuableFinder params[:milestone_title].present? end + def no_milestones? + milestones? && params[:milestone_title] == Milestone::None.title + end + def milestones return @milestones if defined?(@milestones) @milestones = - if milestones? && params[:milestone_title] != Milestone::None.title + if milestones? Milestone.where(title: params[:milestone_title]) else nil @@ -183,7 +187,11 @@ class IssuableFinder def by_milestone(items) if milestones? - items = items.where(milestone_id: milestones.try(:pluck, :id)) + if no_milestones? + items = items.where(milestone_id: [-1, nil]) + else + items = items.where(milestone_id: milestones.try(:pluck, :id)) + end end items @@ -207,13 +215,19 @@ class IssuableFinder def by_label(items) if params[:label_name].present? - label_names = params[:label_name].split(",") + if params[:label_name] == Label::None.title + item_ids = LabelLink.where(target_type: klass.name).pluck(:target_id) - item_ids = LabelLink.joins(:label). - where('labels.title in (?)', label_names). - where(target_type: klass.name).pluck(:target_id) + items = items.where('id NOT IN (?)', item_ids) + else + label_names = params[:label_name].split(",") - items = items.where(id: item_ids) + item_ids = LabelLink.joins(:label). + where('labels.title in (?)', label_names). + where(target_type: klass.name).pluck(:target_id) + + items = items.where(id: item_ids) + end end items diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 8036303851..662ace367b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -93,7 +93,9 @@ module LabelsHelper end def project_labels_options(project) - options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + labels = project.labels.to_a + labels.unshift(Label::None) + options_from_collection_for_select(labels, 'name', 'name', params[:label_name]) end # Required for Gitlab::Markdown::LabelReferenceFilter diff --git a/app/models/label.rb b/app/models/label.rb index 4a22bd5340..14b544b375 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -12,6 +12,9 @@ class Label < ActiveRecord::Base include Referable + # Represents a "No Label" state used for filtering Issues and Merge + # Requests that have no label assigned. + None = Struct.new(:title, :name).new('No Label', 'No Label') DEFAULT_COLOR = '#428BCA' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 8f16773077..6e6d497c1d 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -39,13 +39,13 @@ .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, - class: 'select2 trigger-submit', include_blank: true, + class: 'select2 trigger-submit', include_blank: 'Any', 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, + class: 'select2 trigger-submit', include_blank: 'Any', data: {placeholder: 'Label'}) .pull-right diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index db20b23f87..b164805546 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -6,9 +6,11 @@ describe IssuesFinder do let(:project1) { create(:project) } let(:project2) { create(:project) } let(:milestone) { create(:milestone, project: project1) } + let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } let(:issue2) { create(:issue, author: user, assignee: user, project: project2) } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } + let!(:label_link) { create(:label_link, label: label, target: issue2) } before do project1.team << [user, :master] @@ -48,6 +50,24 @@ describe IssuesFinder do expect(issues).to eq([issue1]) end + it 'should filter by no milestone id' do + params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to match_array([issue2, issue3]) + end + + it 'should filter by label name' do + params = { scope: "all", label_name: label.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to eq([issue2]) + end + + it 'should filter by no label name' do + params = { scope: "all", label_name: Label::None.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to match_array([issue1, issue3]) + end + it 'should be empty for unauthorized user' do params = { scope: "all", state: 'opened' } issues = IssuesFinder.new(nil, params).execute diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0c8d06b705..fb70a36dc0 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -14,6 +14,11 @@ describe LabelsHelper do expect(label).not_to receive(:project) link_to_label(label) end + + it 'includes option for "No Label"' do + result = project_labels_options(project) + expect(result).to include('No Label') + end end context 'without @project set' do From 7ad4521f66b578c0aecf9afb09194626a4c2aa6a Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 5 Oct 2015 21:18:21 +0200 Subject: [PATCH 232/543] Fixes to dropdown, font colors for new project and merge request --- app/assets/stylesheets/generic/buttons.scss | 311 ++++++++++-------- app/assets/stylesheets/generic/common.scss | 8 + app/assets/stylesheets/generic/forms.scss | 24 +- app/assets/stylesheets/generic/issue_box.scss | 2 +- app/assets/stylesheets/generic/selects.scss | 42 ++- app/assets/stylesheets/generic/timeline.scss | 4 +- app/assets/stylesheets/pages/issuable.scss | 9 +- .../stylesheets/pages/merge_requests.scss | 23 +- app/assets/stylesheets/pages/note_form.scss | 8 +- app/assets/stylesheets/pages/notes.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 37 ++- .../projects/merge_requests/_show.html.haml | 2 +- app/views/projects/new.html.haml | 20 +- app/views/projects/notes/_form.html.haml | 2 +- 14 files changed, 303 insertions(+), 191 deletions(-) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index cf76f538e0..50c3de089f 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,8 +1,182 @@ body { text-rendering: geometricPrecision; } + +@mixin btn-default { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 16px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + +@mixin btn-middle { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 24px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + + +@mixin btn-green { + background-color: #28b061; + border: 1px solid #26a65c; + color: #fff; + + &:hover { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:focus { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: #23a158 !important; + border: 1px solid #229954 !important; + color: #fff !important; + } +} + +/*Butons*/ + +@mixin btn-project { + background-color: #f0f2f5; + border-color: #dce0e5; + color: #313236; + + &:hover { + border-color:#dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:focus { + border-color: #dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; + } +} + +@mixin btn-light { + background-color: #fff; + border-color: #dce0e5; + color: #313236; + + &:hover { + border-color:#dce0e5; + background-color: #f0f2f5; + color: #313236; + } + + &:focus { + border-color: #dce0e5; + background-color: #f0f2f5; + color: #313236; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; + } +} + +@mixin btn-remove { + background-color: #f72e60; + border-color: #ee295a; + + &:hover { + background-color: #e82757; + border-color: #e32555; + } + + &:focus { + background-color: #e82757; + border-color: #e32555; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + background-color: #d42450 !important; + border-color: #e12554 !important; + } + +} + + +.btn-info { + @include btn-default; + @include btn-project; +} + +.btn-success { + @include btn-default; + @include btn-green; + +} + .btn { - @extend .btn-default; + @include btn-default; &.btn-new { @extend .btn-success; @@ -90,139 +264,4 @@ body { color: #fff; } } -} - -@mixin btn-info { - @include border-radius(2px); - - border-width: 1px; - border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; - line-height: 18px; - padding: 11px 16px; - letter-spacing: .4px; - - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; - } -} - -@mixin btn-middle { - @include border-radius(2px); - - border-width: 1px; - border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; - line-height: 18px; - padding: 11px 24px; - letter-spacing: .4px; - - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; - } -} - - -@mixin btn-green { - background-color: #28b061; - border: 1px solid #26a65c; - color: #fff; - - &:hover { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:focus { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:active { - @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); - - background-color: #23a158 !important; - border: 1px solid #229954 !important; - color: #fff !important; - } -} - -/*Butons*/ - -@mixin bnt-project { - background-color: #f0f2f5; - border-color: #dce0e5; - color: #313236; - - &:hover { - border-color:#dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:focus { - border-color: #dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; - } -} - -@mixin btn-remove { - background-color: #f72e60; - border-color: #ee295a; - - &:hover { - background-color: #e82757; - border-color: #e32555; - } - - &:focus { - background-color: #e82757; - border-color: #e32555; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - background-color: #d42450 !important; - border-color: #e12554 !important; - } - } \ No newline at end of file diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 45e284542d..03919f15f1 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -394,3 +394,11 @@ table { .dropzone .dz-preview .dz-progress .dz-upload { background: $gl-success !important; } + +.space-right { + margin-right: 10px; +} + +.in-line { + display: inline-block; +} diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 4282832e2b..98a7c3ad8a 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -30,9 +30,6 @@ input[type='text'].danger { } @media (min-width: $screen-sm-min) { - .form-actions { - padding-left: 17%; - } } label { @@ -84,3 +81,24 @@ label { .wiki-content { margin-top: 35px; } + +.form-group .control-label { + font-weight: normal; + font-color: #313236 !important; +} + +.form-control { + @include border-radius (2px); +} + +.form-control::-webkit-input-placeholder{ + color:#7f8fa4; +} + +.input-group { + @include border-radius (2px); + + .input-group-addon { + background-color: #f7f8fa; + } +} diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index b1fb87a683..93377e45e7 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -5,7 +5,7 @@ */ .issue-box { - @include border-radius(3px); + @include border-radius(2px); display: inline-block; padding: 10px $gl-padding; diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index f0860de1c4..633dfea522 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -8,7 +8,7 @@ font-size: $gl-font-size; line-height: 1.42857143; - @include border-radius(4px); + @include border-radius(2px); .select2-arrow { background: #FFF; @@ -18,8 +18,39 @@ } } +.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{ + color: #7f8fa4; + border: 1px solid #e7e9ed; +} + +.select2-drop { + @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); + + padding: 16px 20px; + border: none !important; +} + +.select2-results .select2-result-label { + padding: 16px 20px; +} + +.select2-drop{ + color: #7f8fa4; +} + +.select2-highlighted { + background: #3084bb !important; +} + +.select2-results li.select2-result-with-children > .select2-result-label { + font-weight: 600; + color: #313236; +} + + .select2-container-multi .select2-choices { - @include border-radius(4px); + @include border-radius(2px); border-color: #CCC; } @@ -63,7 +94,7 @@ .ajax-users-dropdown, .ajax-project-users-dropdown { .select2-search { - padding-top: 4px; + padding-top: 2px; } } @@ -97,9 +128,6 @@ } .user-name { } - .user-username { - color: #999; - } } .namespace-result { @@ -115,4 +143,4 @@ .ajax-users-dropdown { min-width: 225px !important; -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index 74bbaabad3..bf21d7fce7 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -10,8 +10,8 @@ margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; - border-bottom: 1px solid #f1f2f4; - border-right: 1px solid #f1f2f4; + border-bottom: 1px solid #ECEEF1; + border-right: 1px solid #ECEEF1; &:last-child { border-bottom: none; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b5c61f7f91..9da085a347 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -54,21 +54,22 @@ margin-top: -15px; padding: 10px 0; margin-bottom: 0; - color: $gl-gray; + color: #5c5d5e; font-size: 16px; .author { - color: $gl-gray; + color: #5c5d5e; } .issue-id { - font-size: 19px; - color: $gl-text-color; + color: #5c5d5e; } } .issue-title { margin: 0; + font-size: 23px; + color: #313236; } .description { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d8c8e5ad0a..fe69d16fc4 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,12 +3,11 @@ * */ .mr-state-widget { - background: #f8fafc; + background: #F7F8FA; margin-bottom: 20px; color: $gl-gray; - border: 1px solid #eef0f2; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); - @include border-radius(3px); + border: 1px solid #dce0e6; + @include border-radius(2px); form { margin-bottom: 0; @@ -76,11 +75,17 @@ .mr-widget-footer { padding: 15px; } + + .normal { + color: #5c5d5e; + } .mr-widget-body { h4 { - font-weight: bold; + font-weight: 600; + font-size: 17px; margin: 5px 0; + color: #313236; } p:last-child { @@ -102,9 +107,7 @@ margin: -$gl-padding; padding: $gl-padding; text-align: center; - border-top: 1px solid #e7e9ed; - margin-top: 18px; - margin-bottom: 3px; + margin-bottom: 1px; } .mr_source_commit, @@ -120,10 +123,12 @@ } .label-branch { - color: #222; + color: #313236; font-family: $monospace_font; font-weight: bold; overflow: hidden; + font-size: 14px; + margin: 0 3px; } .mr-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index fdc2c3332d..dcd1aed719 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -72,12 +72,12 @@ .common-note-form { margin: 0; - background: #f8fafc; + background: #F7F8FA; padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-right: 1px solid #f1f2f4; - border-top: 1px solid #f1f2f4; + border-right: 1px solid #ECEEF1; + border-top: 1px solid #ECEEF1; margin-bottom: -$gl-padding; } @@ -168,7 +168,7 @@ .comment-hints { color: #999; background: #FFF; - padding: 5px; + padding: 7px; margin-top: -11px; border: 1px solid $border-color; font-size: 13px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2a77f065ae..abb03b07f5 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -18,7 +18,7 @@ ul.notes { font-size: 14px; padding-top: 10px; padding-bottom: 10px; - background: #f8fafc; + background: #FDFDFD; .timeline-icon { .avatar { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 818aa10aef..38b17be798 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -13,11 +13,19 @@ .edit_project { fieldset.features { .control-label { - font-weight: bold; + font-weight: normal; } } } +.btn, .commits-compare-switch { + @include btn-light; +} + +.project-edit-content { + padding: 7px; +} + .project-name-holder { .help-inline { vertical-align: top; @@ -92,8 +100,8 @@ margin-bottom: 0px; .btn { - @include bnt-project; - @include btn-info; + @include btn-project; + @include btn-default; .count { display: inline-block; @@ -139,7 +147,11 @@ } } } +.btn-green { + @include btn-green; + cursor: pointer; +} .projects-search-form { .input-group .form-control { @@ -149,7 +161,7 @@ .input-group-btn { .btn { - @include bnt-project; + @include btn-project; @include btn-middle; &:hover { @@ -183,8 +195,8 @@ margin: 0 12px 0 12px; .btn{ - @include bnt-project; - @include btn-info; + @include btn-project; + @include btn-default; } .dropdown-toggle { @@ -251,18 +263,19 @@ margin-bottom: 10px; i { - margin: 0 3px; + margin: 2px 0; font-size: 20px; } .option-title { - font-weight: bold; + font-weight: normal; display: inline-block; + color: #313236; } .option-descr { - margin-left: 36px; - color: $gray; + margin-left: 29px; + color: #54565b; } } } @@ -376,8 +389,8 @@ table.table.protected-branches-list tr.no-border { } .nav > li > a { - @include btn-info; - @include bnt-project; + @include btn-default; + @include btn-project; background-color: transparent; border: 1px solid #f7f8fa; diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0b0f52c653..58d8478744 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -24,7 +24,7 @@ %ul.dropdown-menu %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - .light + .normal %span Request to merge %span.label-branch #{source_branch_with_namespace(@merge_request)} %span into diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index bccea21e7a..1b093c8f51 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -8,7 +8,7 @@ = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f| .form-group.project-name-holder = f.label :path, class: 'control-label' do - %strong Project path + Project path .col-sm-10 .input-group = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true @@ -23,7 +23,6 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} - if import_sources_enabled? - %hr .project-import.js-toggle-container .form-group @@ -35,7 +34,7 @@ %i.fa.fa-github GitHub - else - = link_to '#', class: 'how_to_import_link light btn import_github' do + = link_to '#', class: 'how_to_import_link btn import_github' do %i.fa.fa-github GitHub = render 'github_import_modal' @@ -46,7 +45,7 @@ %i.fa.fa-bitbucket Bitbucket - else - = link_to status_import_bitbucket_path, class: 'how_to_import_link light btn import_bitbucket', "data-no-turbolink" => "true" do + = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do %i.fa.fa-bitbucket Bitbucket = render 'bitbucket_import_modal' @@ -57,7 +56,7 @@ %i.fa.fa-heart GitLab.com - else - = link_to status_import_gitlab_path, class: 'how_to_import_link light btn import_gitlab' do + = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do %i.fa.fa-heart GitLab.com = render 'gitlab_import_modal' @@ -97,7 +96,7 @@ %li To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. - %hr.prepend-botton-10 + .prepend-botton-10 .form-group = f.label :description, class: 'control-label' do @@ -112,10 +111,11 @@ - if current_user.can_create_group? .pull-right - .light - Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-xs" do - Create a group + .light.in-line + .space-right + Need a group for several dependent projects? + = link_to new_group_path, class: "btn btn-xs" do + Create a group .save-project-loader.hide .center diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d99445da59..512ccd48b3 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -14,6 +14,6 @@ .note-form-actions .buttons.clearfix - = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button" + = f.submit 'Add Comment', class: "btn btn-green comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel From f42cfa9e8757161414f88e50d23b1d618bffad1f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 17:00:48 +0200 Subject: [PATCH 233/543] 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 2acb030beaf4a04969340d7d0ede12992f8dd6fe Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 17:21:15 +0200 Subject: [PATCH 234/543] Refactor button css and do some cleanup Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/base/gl_variables.scss | 4 +- app/assets/stylesheets/base/layout.scss | 1 + app/assets/stylesheets/generic/buttons.scss | 131 ++++++++---------- app/assets/stylesheets/generic/forms.scss | 14 +- app/assets/stylesheets/generic/selects.scss | 10 +- app/assets/stylesheets/pages/projects.scss | 15 +- 6 files changed, 76 insertions(+), 99 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 7378d40400..18632da4f2 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -22,8 +22,8 @@ $brand-info: $gl-info; $brand-warning: $gl-warning; $brand-danger: $gl-danger; -$border-radius-base: 3px !default; -$border-radius-large: 5px !default; +$border-radius-base: 2px !default; +$border-radius-large: 2px !default; $border-radius-small: 2px !default; diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index b91c15d891..c7b3b60e76 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -5,6 +5,7 @@ html { body { padding-top: $header-height; + text-rendering: geometricPrecision; } } diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 50c3de089f..086a056bbf 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,10 +1,6 @@ -body { - text-rendering: geometricPrecision; -} - @mixin btn-default { @include border-radius(2px); - + border-width: 1px; border-style: solid; text-transform: uppercase; @@ -13,17 +9,17 @@ body { line-height: 18px; padding: 11px 16px; letter-spacing: .4px; - + &:hover { border-width: 1px; border-style: solid; } - + &:focus { border-width: 1px; border-style: solid; } - + &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); border-width: 1px; @@ -33,7 +29,7 @@ body { @mixin btn-middle { @include border-radius(2px); - + border-width: 1px; border-style: solid; text-transform: uppercase; @@ -42,22 +38,22 @@ body { line-height: 18px; padding: 11px 24px; letter-spacing: .4px; - + &:hover { border-width: 1px; border-style: solid; } - + &:focus { border-width: 1px; border-style: solid; } - + &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); border-width: 1px; border-style: solid; - } + } } @@ -65,118 +61,113 @@ body { background-color: #28b061; border: 1px solid #26a65c; color: #fff; - - &:hover { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:focus { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:active { - @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); - - background-color: #23a158 !important; - border: 1px solid #229954 !important; - color: #fff !important; - } + + &:hover { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:focus { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: #23a158 !important; + border: 1px solid #229954 !important; + color: #fff !important; + } } -/*Butons*/ - -@mixin btn-project { +@mixin btn-gray { background-color: #f0f2f5; border-color: #dce0e5; color: #313236; - + &:hover { border-color:#dce0e5; background-color: #ebeef2; color: #313236; } - + &:focus { border-color: #dce0e5; background-color: #ebeef2; color: #313236; } - + &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; } } -@mixin btn-light { +@mixin btn-white { background-color: #fff; border-color: #dce0e5; color: #313236; - + &:hover { border-color:#dce0e5; background-color: #f0f2f5; color: #313236; } - + &:focus { border-color: #dce0e5; background-color: #f0f2f5; color: #313236; } - + &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; } } -@mixin btn-remove { +@mixin btn-red { background-color: #f72e60; border-color: #ee295a; - + &:hover { background-color: #e82757; border-color: #e32555; } - + &:focus { background-color: #e82757; border-color: #e32555; } - + &:active { @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); background-color: #d42450 !important; border-color: #e12554 !important; } - } -.btn-info { - @include btn-default; - @include btn-project; -} - -.btn-success { - @include btn-default; - @include btn-green; - -} - .btn { @include btn-default; + @include btn-white; + + &.btn-success { + @include btn-green; + } + + &.btn-gray { + @include btn-gray; + } &.btn-new { @extend .btn-success; @@ -264,4 +255,4 @@ body { color: #fff; } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 98a7c3ad8a..0edfe24f19 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -29,9 +29,6 @@ input[type='text'].danger { border-top: 1px solid $border-color; } -@media (min-width: $screen-sm-min) { -} - label { &.control-label { @extend .col-sm-2; @@ -84,20 +81,13 @@ label { .form-group .control-label { font-weight: normal; - font-color: #313236 !important; } -.form-control { - @include border-radius (2px); -} - -.form-control::-webkit-input-placeholder{ - color:#7f8fa4; +.form-control::-webkit-input-placeholder { + color: #7f8fa4; } .input-group { - @include border-radius (2px); - .input-group-addon { background-color: #f7f8fa; } diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 633dfea522..cba621635b 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -26,13 +26,13 @@ .select2-drop { @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); - - padding: 16px 20px; + + padding: 16px; border: none !important; } .select2-results .select2-result-label { - padding: 16px 20px; + padding: 16px; } .select2-drop{ @@ -142,5 +142,5 @@ } .ajax-users-dropdown { - min-width: 225px !important; -} \ No newline at end of file + min-width: 250px !important; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 38b17be798..c36f5a394d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -18,10 +18,6 @@ } } -.btn, .commits-compare-switch { - @include btn-light; -} - .project-edit-content { padding: 7px; } @@ -100,8 +96,7 @@ margin-bottom: 0px; .btn { - @include btn-project; - @include btn-default; + @include btn-gray; .count { display: inline-block; @@ -161,7 +156,7 @@ .input-group-btn { .btn { - @include btn-project; + @include btn-gray; @include btn-middle; &:hover { @@ -195,7 +190,7 @@ margin: 0 12px 0 12px; .btn{ - @include btn-project; + @include btn-gray; @include btn-default; } @@ -390,7 +385,7 @@ table.table.protected-branches-list tr.no-border { .nav > li > a { @include btn-default; - @include btn-project; + @include btn-gray; background-color: transparent; border: 1px solid #f7f8fa; @@ -450,7 +445,7 @@ pre.light-well { .btn-remove { @include btn-middle; - @include btn-remove; + @include btn-red; float: left !important; } From b8e4d6bdbec8631663a017c1b651dc223f0cba16 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 17:32:00 +0200 Subject: [PATCH 235/543] Refactor buttons Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/generic/buttons.scss | 33 ++++++--------------- app/assets/stylesheets/pages/projects.scss | 4 --- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 086a056bbf..62922e6a33 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -156,12 +156,15 @@ } } - .btn { @include btn-default; @include btn-white; - &.btn-success { + &.btn-success, + &.btn-new, + &.btn-create, + &.btn-save, + &.btn-green { @include btn-green; } @@ -169,20 +172,10 @@ @include btn-gray; } - &.btn-new { - @extend .btn-success; - } - - &.btn-create { - @extend .btn-success; - } - - &.btn-save { - @extend .btn-success; - } - - &.btn-remove { - @extend .btn-danger; + &.btn-danger, + &.btn-remove, + &.btn-red { + @include btn-red; } &.btn-cancel { @@ -212,14 +205,6 @@ margin-right: 0px; } } - - &.btn-save { - @extend .btn-primary; - } - - &.btn-new, &.btn-create { - @extend .btn-success; - } } .btn-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c36f5a394d..0031ab5151 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -142,11 +142,7 @@ } } } -.btn-green { - @include btn-green; - cursor: pointer; -} .projects-search-form { .input-group .form-control { From 7f63a8787ce454a61f72393ccbe22fc283ba8313 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 7 Oct 2015 17:54:49 +0200 Subject: [PATCH 236/543] Fix tests and few CI features Signed-off-by: Dmitriy Zaporozhets --- app/controllers/ci/projects_controller.rb | 1 + app/controllers/projects/commit_controller.rb | 2 +- app/views/projects/ci_settings/_form.html.haml | 2 +- config/routes.rb | 2 +- spec/requests/ci/builds_spec.rb | 17 ----------------- spec/requests/ci/commits_spec.rb | 16 ---------------- 6 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 spec/requests/ci/builds_spec.rb delete mode 100644 spec/requests/ci/commits_spec.rb diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 5484ae643a..7777aa1803 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,5 +1,6 @@ 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 :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index c08a90bddf..7886f3c6de 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -42,7 +42,7 @@ class Projects::CommitController < Projects::ApplicationController @ci_commit = @project.ci_commit(@commit.sha) @ci_commit.builds.running_or_pending.each(&:cancel) - redirect_to namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_to ci_namespace_project_commit_path(project.namespace, project, commit.sha) end diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml index 3412555020..d711413c6b 100644 --- a/app/views/projects/ci_settings/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -8,7 +8,7 @@ Edit your #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} -- if @repository +- unless @project.empty_repo? %p Paste build status image for #{@repository.root_ref} with next link = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do diff --git a/config/routes.rb b/config/routes.rb index e91b09de9b..efaf818a9e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -465,7 +465,7 @@ Gitlab::Application.routes.draw do member do get :branches get :ci - post :cancel_builds + get :cancel_builds end end diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb deleted file mode 100644 index f68116c52a..0000000000 --- a/spec/requests/ci/builds_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe "Builds" do - before do - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - end - - describe "GET /:project/builds/:id/status.json" do - before do - get status_ci_project_build_path(@commit.project, @build), format: :json - end - - it { expect(response.status).to eq(200) } - it { expect(response.body).to include(@build.sha) } - end -end diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb deleted file mode 100644 index f43a3982d7..0000000000 --- a/spec/requests/ci/commits_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe "Commits" do - before do - @commit = FactoryGirl.create :ci_commit - end - - describe "GET /:project/refs/:ref_name/commits/:id/status.json" do - before do - get status_ci_project_commits_path(@commit.project, @commit.sha), format: :json - end - - it { expect(response.status).to eq(200) } - it { expect(response.body).to include(@commit.sha) } - end -end From 72afcbcd37f35e02544290977c2c91ae37c12c01 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 7 Oct 2015 19:19:23 +0200 Subject: [PATCH 237/543] 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 38fbfb9fe6d52e1a36b16ccc2a9da9ae01561cca Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 18:03:14 -0400 Subject: [PATCH 238/543] Require jquery.turbolinks in all JS specs Side-effect: This simplifies the requiresInput handler --- app/assets/javascripts/behaviors/requires_input.js.coffee | 3 +-- spec/javascripts/spec_helper.coffee | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee index 8318fe435b..79d750d184 100644 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -34,6 +34,5 @@ $.fn.requiresInput = -> $form.on 'change input', fieldSelector, requireInput -# Triggered on standard document `ready` and on Turbolinks `page:load` events -$(document).on 'ready page:load', -> +$ -> $('form.js-requires-input').requiresInput() diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee index 47b41dd2c8..90b02a6aec 100644 --- a/spec/javascripts/spec_helper.coffee +++ b/spec/javascripts/spec_helper.coffee @@ -9,6 +9,7 @@ # require the specific files that are being used in the spec that tests them. #= require jquery +#= require jquery.turbolinks #= require bootstrap #= require underscore From 01d0926bc16bb82ef676c35a5a61b8e90a82654c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 18:19:11 -0400 Subject: [PATCH 239/543] Add "Quick Submit" JS behavior --- .../behaviors/quick_submit.js.coffee | 29 +++++++++ .../behaviors/quick_submit_spec.js.coffee | 65 +++++++++++++++++++ .../fixtures/behaviors/quick_submit.html.haml | 6 ++ 3 files changed, 100 insertions(+) create mode 100644 app/assets/javascripts/behaviors/quick_submit.js.coffee create mode 100644 spec/javascripts/behaviors/quick_submit_spec.js.coffee create mode 100644 spec/javascripts/fixtures/behaviors/quick_submit.html.haml diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee new file mode 100644 index 0000000000..dc9bd4d94c --- /dev/null +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -0,0 +1,29 @@ +# Quick Submit behavior +# +# When an input field with the `js-quick-submit` class receives a "Meta+Enter" +# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is +# submitted. +# +#= require extensions/jquery +# +# ### Example Markup +# +#
+# +# +#
+# +$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> + return if e.repeat + return unless e.keyCode == 13 # Enter + + if navigator.userAgent.match(/Macintosh/) + return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) + else + return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) + + e.preventDefault() + + $form = $(e.target).closest('form') + $form.find('input[type=submit], button[type=submit]').disable() + $form.submit() diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee new file mode 100644 index 0000000000..6beef28741 --- /dev/null +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -0,0 +1,65 @@ +#= require behaviors/quick_submit + +describe 'Quick Submit behavior', -> + fixture.preload('behaviors/quick_submit.html') + + beforeEach -> + fixture.load('behaviors/quick_submit.html') + + # Prevent a form submit from moving us off the testing page + $('form').submit (e) -> e.preventDefault() + + @spies = { + submit: spyOnEvent('form', 'submit') + } + + it 'does not respond to other keyCodes', -> + $('input').trigger(keydownEvent(keyCode: 32)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + it 'does not respond to Enter alone', -> + $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + it 'disables submit buttons', -> + $('textarea').trigger(keydownEvent()) + + expect($('input[type=submit]')).toBeDisabled() + expect($('button[type=submit]')).toBeDisabled() + + # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + # only run the tests that apply to the current platform + if navigator.userAgent.match(/Macintosh/) + it 'responds to Meta+Enter', -> + $('input').trigger(keydownEvent()) + + expect(@spies.submit).toHaveBeenTriggered() + + it 'excludes other modifier keys', -> + $('input').trigger(keydownEvent(altKey: true)) + $('input').trigger(keydownEvent(ctrlKey: true)) + $('input').trigger(keydownEvent(shiftKey: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + else + it 'responds to Ctrl+Enter', -> + $('input').trigger(keydownEvent()) + + expect(@spies.submit).toHaveBeenTriggered() + + it 'excludes other modifier keys', -> + $('input').trigger(keydownEvent(altKey: true)) + $('input').trigger(keydownEvent(metaKey: true)) + $('input').trigger(keydownEvent(shiftKey: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + keydownEvent = (options) -> + if navigator.userAgent.match(/Macintosh/) + defaults = { keyCode: 13, metaKey: true } + else + defaults = { keyCode: 13, ctrlKey: true } + + $.Event('keydown', $.extend({}, defaults, options)) diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml new file mode 100644 index 0000000000..b80a28a33e --- /dev/null +++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml @@ -0,0 +1,6 @@ +%form{ action: '/foo' } + %input.js-quick-submit{ type: 'text' } + %textarea.js-quick-submit + + %input{ type: 'submit'} Submit + %button.btn{ type: 'submit' } Submit From 2ed5a5b70df6e4c9b4b9afd0e1cac12f267676e8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 18:20:00 -0400 Subject: [PATCH 240/543] Remove "quick submit" behavior specific to the Notes JS --- app/assets/javascripts/notes.js.coffee | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 4b9f0d6891..ea75c656bc 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -63,12 +63,6 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange - # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. - $(document).on 'keydown', '.js-note-text', (e) -> - return if e.originalEvent.repeat - if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) - $(@).closest('form').submit() - cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -82,7 +76,6 @@ class @Notes $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(document).off "keydown", ".js-note-text" $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" From de3ff174e70a9048fc8f23cd5ee0c709471f3651 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 18:20:43 -0400 Subject: [PATCH 241/543] Quick Submit all the things! Adds quick submit behavior to the forms for: - Blobs ("new file") - Commit messages - Issuables - Notes - Labels - Milestones - Wikis --- app/views/projects/blob/_editor.html.haml | 2 +- app/views/projects/blob/new.html.haml | 2 +- app/views/projects/labels/_form.html.haml | 2 +- app/views/projects/milestones/_form.html.haml | 4 ++-- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 2 +- app/views/projects/wikis/_form.html.haml | 2 +- app/views/shared/_commit_message_container.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 9c3e1703c8..f1ad0c3c40 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -11,7 +11,7 @@ - if current_action?(:new) || current_action?(:create) \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name' + required: true, class: 'form-control new-file-name js-quick-submit' .pull-right = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index d7987e24ef..7975137c37 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -15,7 +15,7 @@ = label_tag 'branch', class: 'control-label' do Branch .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + = text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 534c545329..4cf13492e9 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -10,7 +10,7 @@ .form-group = f.label :title, class: 'control-label' .col-sm-10 - = f.text_field :title, class: "form-control", required: true + = f.text_field :title, class: "form-control js-quick-submit", required: true .form-group = f.label :color, "Background Color", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 74e9668052..255ddab479 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -16,13 +16,13 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control", required: true + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true %p.hint Required .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' .hint .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a0e26f9827..a21c019986 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit' = render 'projects/notes/hints' .note-form-actions diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 512ccd48b3..13dfa0a1bb 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit' = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 05d754adbe..261d4a92d7 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -22,7 +22,7 @@ = f.label :content, class: 'control-label' .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' + = 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' }. diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 5071ff640f..cc3f1268f8 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -6,7 +6,7 @@ .max-width-marker = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text]), - class: 'form-control', placeholder: local_assigns[:placeholder], + class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder], required: true, rows: (local_assigns[:rows] || 3) - if local_assigns[:hint] %p.hint diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 33ec726e93..594e54f404 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -10,7 +10,7 @@ %strong= 'Title *' .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad js-gfm-input', required: true + class: 'form-control pad js-gfm-input js-quick-submit', required: true - if issuable.is_a?(MergeRequest) %p.help-block @@ -26,7 +26,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' + classes: 'description form-control js-quick-submit' .col-sm-12.hint .pull-left Parsed with From f96c6c43c49c8c423f42a99ae27e02d47f04c748 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 5 Oct 2015 18:31:18 -0400 Subject: [PATCH 242/543] Add CHANGELOG entry for "Quick Submit" [ci skip] --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 8ab2d9f08d..74d9ff9732 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,8 @@ v 8.1.0 (unreleased) - Use commit status in merge request widget as preffered 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 + Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux. v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From fcf99d0d09d63d0662290658ebc1e366bfc11bf0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 15:19:26 -0400 Subject: [PATCH 243/543] Check originalEvent.repeat --- .../javascripts/behaviors/quick_submit.js.coffee | 2 +- spec/javascripts/behaviors/quick_submit_spec.js.coffee | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index dc9bd4d94c..0748eed763 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -14,7 +14,7 @@ # # $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> - return if e.repeat + return if e.originalEvent.repeat return unless e.keyCode == 13 # Enter if navigator.userAgent.match(/Macintosh/) diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee index 6beef28741..fd20f67474 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -23,6 +23,11 @@ describe 'Quick Submit behavior', -> expect(@spies.submit).not.toHaveBeenTriggered() + it 'does not respond to repeated events', -> + $('input').trigger(keydownEvent(repeat: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + it 'disables submit buttons', -> $('textarea').trigger(keydownEvent()) @@ -62,4 +67,7 @@ describe 'Quick Submit behavior', -> else defaults = { keyCode: 13, ctrlKey: true } - $.Event('keydown', $.extend({}, defaults, options)) + args = $.extend({}, defaults, options) + originalEvent = new KeyboardEvent('keydown', args) + + $.Event('keydown', $.extend({}, args, {originalEvent: originalEvent})) From c3d9c55b23183a51c941800d94ed0ff085e5a3b8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 15:46:18 -0400 Subject: [PATCH 244/543] Add gitlab:two_factor:disable_for_all_users task --- lib/tasks/gitlab/two_factor.rake | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/tasks/gitlab/two_factor.rake diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake new file mode 100644 index 0000000000..acd4b7da39 --- /dev/null +++ b/lib/tasks/gitlab/two_factor.rake @@ -0,0 +1,10 @@ +namespace :gitlab do + namespace :two_factor do + desc "GitLab | Disable Two-factor authentication (2FA) for all users" + task disable_for_all_users: :environment do + User.with_two_factor.find_each do |user| + user.disable_two_factor! + end + end + end +end From 5a28a5b72634ba180180dfeaeca1d2b0d988e177 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 15:46:54 -0400 Subject: [PATCH 245/543] Add docs for gitlab:two_factor:disable_for_all_users task --- doc/raketasks/user_management.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 4fbd20762d..629d38efc5 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -56,3 +56,17 @@ bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production ``` block_auto_created_users: false ``` + +## Disable Two-factor Authentication (2FA) for all users + +This task will disable 2FA for all users that have it enabled. This can be +useful if GitLab's `.secret` file has been lost and users are unable to login, +for example. + +```bash +# omnibus-gitlab +sudo gitlab-rake gitlab:two_factor:disable_for_all_users + +# installation from source +bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production +``` From 85c6a3743abe5683c2317f1957a9f047ad2b4b8e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 12:30:10 +0200 Subject: [PATCH 246/543] Added methods for detecting MySQL/PostgreSQL These two methods remove the need for manually going into ActiveRecord::Base.connection all over the place. --- lib/gitlab/database.rb | 11 +++++++++++ spec/lib/gitlab/database_spec.rb | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 lib/gitlab/database.rb create mode 100644 spec/lib/gitlab/database_spec.rb diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb new file mode 100644 index 0000000000..741a52714a --- /dev/null +++ b/lib/gitlab/database.rb @@ -0,0 +1,11 @@ +module Gitlab + module Database + def self.mysql? + ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' + end + + def self.postgresql? + ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb new file mode 100644 index 0000000000..7cdebdf209 --- /dev/null +++ b/spec/lib/gitlab/database_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Database do + # These are just simple smoke tests to check if the methods work (regardless + # of what they may return). + describe '.mysql?' do + subject { described_class.mysql? } + + it { is_expected.to satisfy { |val| val == true || val == false } } + end + + describe '.postgresql?' do + subject { described_class.postgresql? } + + it { is_expected.to satisfy { |val| val == true || val == false } } + end +end From 1190d0ab3dc7a3025bf55b666f34d1a0b51a8d89 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 14:03:18 +0200 Subject: [PATCH 247/543] Added concern for case-insensitive WHERE queries On PostgreSQL these queries use LOWER(...) to compare columns and values. For MySQL a regular WHERE is performed as MySQL is already case-insensitive. --- app/models/concerns/case_sensitivity.rb | 28 +++ spec/models/concerns/case_sensitivity_spec.rb | 176 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 app/models/concerns/case_sensitivity.rb create mode 100644 spec/models/concerns/case_sensitivity_spec.rb diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb new file mode 100644 index 0000000000..49d350e092 --- /dev/null +++ b/app/models/concerns/case_sensitivity.rb @@ -0,0 +1,28 @@ +# Concern for querying columns with specific case sensitivity handling. +module CaseSensitivity + extend ActiveSupport::Concern + + module ClassMethods + # Queries the given columns regardless of the casing used. + # + # Unlike other ActiveRecord methods this method only operates on a Hash. + def case_insensitive_where(params) + criteria = self + cast_lower = Gitlab::Database.postgresql? + + params.each do |key, value| + column = ActiveRecord::Base.connection.quote_table_name(key) + + if cast_lower + condition = "LOWER(#{column}) = LOWER(:value)" + else + condition = "#{column} = :value" + end + + criteria = criteria.where(condition, value: value) + end + + criteria + end + end +end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb new file mode 100644 index 0000000000..8b9f50aada --- /dev/null +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' + +describe CaseSensitivity do + describe '.case_insensitive_where' do + let(:connection) { ActiveRecord::Base.connection } + let(:model) { Class.new { include CaseSensitivity } } + + describe 'using PostgreSQL' do + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('"bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('"foo"."baz"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + + expect(got).to eq(final) + end + end + end + + describe 'using MySQL' do + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('`bar`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER(`bar`) = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('`foo`.`baz`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER(`foo`.`baz`) = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + + expect(got).to eq(final) + end + end + end + end +end From dbf9ccbcfe788beb24cda34b0d86b7e12ecdf4bb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 17:59:10 -0400 Subject: [PATCH 248/543] Check originalEvent.repeat *and* event.repeat phantomjs < 2.0 doesn't support creating `KeyboardEvent` so the tests were failing on CI --- app/assets/javascripts/behaviors/quick_submit.js.coffee | 2 +- spec/javascripts/behaviors/quick_submit_spec.js.coffee | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 0748eed763..4ec8531d58 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -14,7 +14,7 @@ # # $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> - return if e.originalEvent.repeat + return if (e.originalEvent && e.originalEvent.repeat) || e.repeat return unless e.keyCode == 13 # Enter if navigator.userAgent.match(/Macintosh/) diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee index fd20f67474..09708c12ed 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -67,7 +67,4 @@ describe 'Quick Submit behavior', -> else defaults = { keyCode: 13, ctrlKey: true } - args = $.extend({}, defaults, options) - originalEvent = new KeyboardEvent('keydown', args) - - $.Event('keydown', $.extend({}, args, {originalEvent: originalEvent})) + $.Event('keydown', $.extend({}, defaults, options)) From e0f9d3a547e340d229f6164c917d19955a10b371 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 18:25:36 -0400 Subject: [PATCH 249/543] 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 511fb3ff2ac08537675ea44af0e340921368fc82 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 21:51:05 -0400 Subject: [PATCH 250/543] Update slack-notifier to ~> 1.2.0 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 26ffb8c4b3..67a8a4b121 100644 --- a/Gemfile +++ b/Gemfile @@ -163,7 +163,7 @@ gem "gitlab-flowdock-git-hook", "~> 1.0.1" gem "gemnasium-gitlab-service", "~> 0.2" # Slack integration -gem "slack-notifier", "~> 1.0.0" +gem "slack-notifier", "~> 1.2.0" # Asana integration gem 'asana', '~> 0.0.6' diff --git a/Gemfile.lock b/Gemfile.lock index fc9244304a..9cf834cf76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -666,7 +666,7 @@ GEM rack-protection (~> 1.4) tilt (>= 1.3, < 3) six (0.2.0) - slack-notifier (1.0.0) + slack-notifier (1.2.1) slim (2.0.3) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) @@ -912,7 +912,7 @@ DEPENDENCIES simplecov (~> 0.10.0) sinatra (~> 1.4.4) six (~> 0.2.0) - slack-notifier (~> 1.0.0) + slack-notifier (~> 1.2.0) slim (~> 2.0.2) spinach-rails (~> 0.2.1) spring (~> 1.3.6) From a40cd65d3bd4f284e16c7e8b02b6157d4a882811 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 21:54:15 -0400 Subject: [PATCH 251/543] Update tinder to ~> 1.10.0 --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 26ffb8c4b3..3739df0e7f 100644 --- a/Gemfile +++ b/Gemfile @@ -151,7 +151,7 @@ gem 'version_sorter', '~> 2.0.0' gem "redis-rails", '~> 4.0.0' # Campfire integration -gem 'tinder', '~> 1.9.2' +gem 'tinder', '~> 1.10.0' # HipChat integration gem 'hipchat', '~> 1.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index fc9244304a..3cd08cb77b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -182,8 +182,8 @@ GEM factory_girl_rails (4.3.0) factory_girl (~> 4.3.0) railties (>= 3.0.0) - faraday (0.8.10) - multipart-post (~> 1.2.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) faraday_middleware (0.10.0) faraday (>= 0.7.4, < 0.10) fastercsv (1.5.5) @@ -335,7 +335,7 @@ GEM activesupport (>= 4.0.1) haml (>= 3.1, < 5.0) railties (>= 4.0.1) - hashie (2.1.2) + hashie (3.4.2) highline (1.6.21) hike (1.2.3) hipchat (1.5.2) @@ -396,7 +396,7 @@ GEM mousetrap-rails (1.4.6) multi_json (1.11.2) multi_xml (0.5.5) - multipart-post (1.2.0) + multipart-post (2.0.0) mysql2 (0.3.20) nenv (0.2.0) nested_form (0.3.2) @@ -721,13 +721,13 @@ GEM timers (4.0.4) hitimes timfel-krb5-auth (0.8.3) - tinder (1.9.4) + tinder (1.10.1) eventmachine (~> 1.0) - faraday (~> 0.8.9) + faraday (~> 0.9.0) faraday_middleware (~> 0.9) - hashie (>= 1.0, < 3) + hashie (>= 1.0) json (~> 1.8.0) - mime-types (~> 1.19) + mime-types multi_json (~> 1.7) twitter-stream (~> 0.1) tins (1.6.0) @@ -927,7 +927,7 @@ DEPENDENCIES teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.2.2) thin (~> 1.6.1) - tinder (~> 1.9.2) + tinder (~> 1.10.0) turbolinks (~> 2.5.0) uglifier (~> 2.3.2) underscore-rails (~> 1.4.4) From 53f43910addad75e14a0860235a4ade96bfc5009 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 21:59:16 -0400 Subject: [PATCH 252/543] Update haml-rails to ~> 0.9.0 --- Gemfile | 2 +- Gemfile.lock | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 26ffb8c4b3..205d9044f3 100644 --- a/Gemfile +++ b/Gemfile @@ -80,7 +80,7 @@ gem 'enumerize', '~> 0.7.0' gem "kaminari", "~> 0.16.3" # HAML -gem "haml-rails", '~> 0.5.3' +gem "haml-rails", '~> 0.9.0' # Files attachments gem "carrierwave", '~> 0.9.0' diff --git a/Gemfile.lock b/Gemfile.lock index fc9244304a..56466ad95c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -330,10 +330,11 @@ GEM rspec (>= 2.14, < 4.0) haml (4.0.7) tilt - haml-rails (0.5.3) + haml-rails (0.9.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) - haml (>= 3.1, < 5.0) + haml (>= 4.0.6, < 5.0) + html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (2.1.2) highline (1.6.21) @@ -345,6 +346,11 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) + html2haml (2.0.0) + erubis (~> 2.7.0) + haml (~> 4.0.0) + nokogiri (~> 1.6.0) + ruby_parser (~> 3.5) http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.5.3) @@ -845,7 +851,7 @@ DEPENDENCIES grape-entity (~> 0.4.2) growl guard-rspec (~> 4.2.0) - haml-rails (~> 0.5.3) + haml-rails (~> 0.9.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) From f9744ea9a42f206f00b16fd7866540bb9edcd93c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 22:08:30 -0400 Subject: [PATCH 253/543] Update rack-cors to ~> 0.4.0 --- Gemfile | 6 +++--- Gemfile.lock | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 26ffb8c4b3..05306374f1 100644 --- a/Gemfile +++ b/Gemfile @@ -65,9 +65,9 @@ gem 'gollum-lib', '~> 4.0.2' gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API -gem "grape", "~> 0.6.1" -gem "grape-entity", "~> 0.4.2" -gem 'rack-cors', '~> 0.2.9', require: 'rack/cors' +gem 'grape', '~> 0.6.1' +gem 'grape-entity', '~> 0.4.2' +gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Format dates and times # based on human-friendly examples diff --git a/Gemfile.lock b/Gemfile.lock index fc9244304a..5336f4bf95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -496,7 +496,7 @@ GEM rack (>= 0.4) rack-attack (4.3.0) rack - rack-cors (0.2.9) + rack-cors (0.4.0) rack-mini-profiler (0.9.7) rack (>= 1.1.3) rack-mount (0.8.3) @@ -883,7 +883,7 @@ DEPENDENCIES pry-rails quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) - rack-cors (~> 0.2.9) + rack-cors (~> 0.4.0) rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) rails (= 4.1.12) From 5e01e1bb901c967e71d165a1e664b49538277c10 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 22:08:37 -0400 Subject: [PATCH 254/543] Use `methods: :any` in our rack-cors configuration This was added in rack-cors 0.4.0 and allows all types of CORS requests to the API. --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index a96e22211e..bfa2a809dd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -74,7 +74,7 @@ module Gitlab origins '*' resource '/api/*', headers: :any, - methods: [:get, :post, :options, :put, :delete], + methods: :any, expose: ['Link'] end end From 26cfb98e4fcfb1bea1b4a5c723940e2a51ef10dc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 22:37:48 -0400 Subject: [PATCH 255/543] Update whenever to ~> 0.9.4 --- Gemfile | 2 +- Gemfile.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 26ffb8c4b3..e592d5ab20 100644 --- a/Gemfile +++ b/Gemfile @@ -301,7 +301,7 @@ gem 'activerecord-session_store', '~> 0.1.0' gem "nested_form", '~> 0.3.2' # Scheduled -gem 'whenever', '~> 0.8.4', require: false +gem 'whenever', '~> 0.9.4', require: false # OAuth gem 'oauth2', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index fc9244304a..29ec33bf52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -770,8 +770,7 @@ GEM websocket-driver (0.6.2) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - whenever (0.8.4) - activesupport (>= 2.3.4) + whenever (0.9.4) chronic (>= 0.6.3) wikicloth (0.8.1) builder @@ -937,7 +936,7 @@ DEPENDENCIES version_sorter (~> 2.0.0) virtus (~> 1.0.1) webmock (~> 1.21.0) - whenever (~> 0.8.4) + whenever (~> 0.9.4) wikicloth (= 0.8.1) BUNDLED WITH From 63f2510740d8f8756b04dbdf0f3b78d1f6a1ff84 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 8 Oct 2015 11:00:10 +0200 Subject: [PATCH 256/543] Fix build highlight Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/ci/builds.scss | 1 - app/assets/stylesheets/ci/xterm.scss | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss index a27dd0db58..74dc3e321c 100644 --- a/app/assets/stylesheets/ci/builds.scss +++ b/app/assets/stylesheets/ci/builds.scss @@ -73,4 +73,3 @@ margin-bottom: 2px; } } - diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/ci/xterm.scss index 532dede0b2..9a50096c0d 100644 --- a/app/assets/stylesheets/ci/xterm.scss +++ b/app/assets/stylesheets/ci/xterm.scss @@ -1,4 +1,4 @@ -.ci-body { +.build-page { // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg // see also: https://gist.github.com/jasonm23/2868981 From e6419cecf4221d44be6e80a41ed668bd79f247fb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 8 Oct 2015 12:58:47 +0200 Subject: [PATCH 257/543] 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 258/543] 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 057b1c317bd27461cce93d93399909e67869b159 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 8 Oct 2015 13:29:14 +0200 Subject: [PATCH 259/543] Add new GitLab colors Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/base/variables.scss | 62 ++++++- app/assets/stylesheets/generic/buttons.scss | 154 +++++------------- app/assets/stylesheets/pages/commits.scss | 3 +- app/views/help/ui.html.haml | 1 + .../projects/merge_requests/_show.html.haml | 2 +- 5 files changed, 98 insertions(+), 124 deletions(-) diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index befd63832d..eb9a296638 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -23,15 +23,67 @@ $gl-gray: #7f8fa4; $gl-padding: 16px; $gl-avatar-size: 46px; +/* + * Color schema + */ + +$white-light: #FFFFFF; +$white-normal: #DCE0E5; +$white-dark: #E4E7ED; + +$gray-light: #F0F2F5; +$gray-normal: #DCE0E5; +$gray-dark: #E4E7ED; + +$green-light: #31AF64; +$green-normal: #2FAA60; +$green-dark: #2CA05B; + +$blue-light: #2EA8E5; +$blue-normal: #2D9FD8; +$blue-dark: #2897CE; + +$orange-light: #FC6443; +$orange-normal: #E75E40; +$orange-dark: #CE5237; + +$red-light: #F43263; +$red-normal: #E52C5A; +$red-dark: #D22852; + +$border-white-light: #E3E7EC; +$border-white-normal: #D6DAE2; +$border-white-dark: #C6CACF; + +$border-gray-light: #DCE0E5; +$border-gray-normal: #D6DAE2; +$border-gray-dark: #C6CACF; + +$border-green-light: #2FAA60; +$border-green-normal: #2CA05B; +$border-green-dark: #279654; + +$border-blue-light: #2D9FD8; +$border-blue-normal: #2897CE; +$border-blue-dark: #258DC1; + +$border-orange-light: #ED5C3D; +$border-orange-normal: #CE5237; +$border-orange-dark: #C14E35; + +$border-red-light: #E52C5A; +$border-red-normal: #D22852; +$border-red-dark: #CA264F; + /* * State colors: */ -$gl-primary: #446e9b; -$gl-success: #44c679; -$gl-info: #00aaff; -$gl-warning: #EB9532; -$gl-danger: #d9534f; +$gl-primary: $blue-normal; +$gl-success: $green-normal; +$gl-info: $blue-normal; +$gl-warning: $orange-normal; +$gl-danger: $red-normal; /* * Commit Diff Colors diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 62922e6a33..11acbe3adf 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,6 +1,5 @@ @mixin btn-default { @include border-radius(2px); - border-width: 1px; border-style: solid; text-transform: uppercase; @@ -10,150 +9,62 @@ padding: 11px 16px; letter-spacing: .4px; - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - + &:focus, &:active { + outline: none; @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; } } @mixin btn-middle { + @include btn-default; @include border-radius(2px); - - border-width: 1px; - border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; - line-height: 18px; padding: 11px 24px; - letter-spacing: .4px; - - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; - } } +@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { + background-color: $light; + border-color: $border-light; + color: $color; -@mixin btn-green { - background-color: #28b061; - border: 1px solid #26a65c; - color: #fff; - - &:hover { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - + &:hover, &:focus { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; + background-color: $normal; + border-color: $border-normal; + color: $color; } &:active { @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); - background-color: #23a158 !important; - border: 1px solid #229954 !important; - color: #fff !important; + background-color: $dark; + border-color: $border-dark; + color: $color; } } -@mixin btn-gray { - background-color: #f0f2f5; - border-color: #dce0e5; - color: #313236; - - &:hover { - border-color:#dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:focus { - border-color: #dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; - } +@mixin btn-green { + @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF); } -@mixin btn-white { - background-color: #fff; - border-color: #dce0e5; - color: #313236; +@mixin btn-blue { + @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF); +} - &:hover { - border-color:#dce0e5; - background-color: #f0f2f5; - color: #313236; - } - - &:focus { - border-color: #dce0e5; - background-color: #f0f2f5; - color: #313236; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; - } +@mixin btn-orange { + @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF); } @mixin btn-red { - background-color: #f72e60; - border-color: #ee295a; + @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF); +} - &:hover { - background-color: #e82757; - border-color: #e32555; - } +@mixin btn-gray { + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236); +} - &:focus { - background-color: #e82757; - border-color: #e32555; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - background-color: #d42450 !important; - border-color: #e12554 !important; - } +@mixin btn-white { + @include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236); } .btn { @@ -172,6 +83,15 @@ @include btn-gray; } + &.btn-primary, + &.btn-info { + @include btn-blue; + } + + &.btn-warning { + @include btn-orange; + } + &.btn-danger, &.btn-remove, &.btn-red { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index de2ae93df3..4e121b95d1 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,5 +1,6 @@ .commits-compare-switch{ - @extend .btn; + @include btn-default; + @include btn-white; background: image-url("switch_icon.png") no-repeat center center; text-indent: -9999px; float: left; diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 7c89457ace..1bd271477e 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -107,6 +107,7 @@ .example %button.btn.btn-default{:type => "button"} Default + %button.btn.btn-gray{:type => "button"} Gray %button.btn.btn-primary{:type => "button"} Primary %button.btn.btn-success{:type => "button"} Success %button.btn.btn-info{:type => "button"} Info diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 58d8478744..e7ac7a0eaa 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -34,7 +34,7 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.open? && @merge_request.can_be_merged? - .light + .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" From dc400a7414e5b26893d002ce42bfaf15c0ccae79 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 8 Oct 2015 13:47:59 +0200 Subject: [PATCH 260/543] Improve help/ui page Signed-off-by: Dmitriy Zaporozhets --- app/views/help/ui.html.haml | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 1bd271477e..2169a821fb 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -14,6 +14,8 @@ = link_to 'Lists', '#lists' %li = link_to 'Tables', '#tables' + %li + = link_to 'Nav', '#nav' %li = link_to 'Buttons', '#buttons' %li @@ -30,16 +32,31 @@ %h2#blocks Blocks %h3 - %code .well + %code .gray-content-block - .well - %h4 Something + + .gray-content-block.middle-block + %h4 Normal block inside content + = lorem + + .gray-content-block.second-block + %h4 Second block = lorem %h2#lists Lists + %h3 + %code .content-list + %ul.content-list + %li + One item + %li + One item + %li + One item + %h3 %code .well-list %ul.well-list @@ -102,6 +119,34 @@ %td the Bird %td @twitter + %h2#navs Navigation + + %h3 + %code .center-top-menu + .example + %ul.center-top-menu + %li.active + %a Open + %li + %a Closed + + %h3 + %code .btn-group.btn-group-next + .example + %div.btn-group.btn-group-next + %a.btn.active Open + %a.btn Closed + + + %h3 + %code .nav.nav-tabs + .example + %ul.nav.nav-tabs + %li.active + %a Open + %li + %a Closed + %h2#buttons Buttons From 03417456f0b7db408bfefd28e5b9342889b7f711 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 17:37:39 +0200 Subject: [PATCH 261/543] Revamp finding projects by namespaces By using a JOIN we can remove the need for using 2 separate queries to find a project by its namespace. Combined with an index (only needed for PostgreSQL) this reduces the query time from ~245 ms (~520 ms for the first call) down to roughly 10 ms (~15 ms for the first call). --- app/models/concerns/case_sensitivity.rb | 2 +- app/models/project.rb | 16 ++++--- ..._namespaces_projects_path_lower_indexes.rb | 17 ++++++++ db/schema.rb | 2 +- spec/benchmarks/models/project_spec.rb | 20 +++++++++ spec/models/concerns/case_sensitivity_spec.rb | 43 ++++++++++++------- 6 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb create mode 100644 spec/benchmarks/models/project_spec.rb diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 49d350e092..fe0cea8465 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -6,7 +6,7 @@ module CaseSensitivity # Queries the given columns regardless of the casing used. # # Unlike other ActiveRecord methods this method only operates on a Hash. - def case_insensitive_where(params) + def iwhere(params) criteria = self cast_lower = Gitlab::Database.postgresql? diff --git a/app/models/project.rb b/app/models/project.rb index bb47b9abb0..f75082a35d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,6 +40,7 @@ class Project < ActiveRecord::Base include Referable include Sortable include AfterCommitQueue + include CaseSensitivity extend Gitlab::ConfigHelper extend Enumerize @@ -235,13 +236,18 @@ class Project < ActiveRecord::Base end def find_with_namespace(id) - return nil unless id.include?('/') + namespace_path, project_path = id.split('/') - id = id.split('/') - namespace = Namespace.by_path(id.first) - return nil unless namespace + return nil if !namespace_path || !project_path - where(namespace_id: namespace.id).where("LOWER(projects.path) = :path", path: id.second.downcase).first + # 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. + joins(:namespace). + iwhere('namespaces.path' => namespace_path). + iwhere('projects.path' => project_path). + take end def visibility_levels diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb new file mode 100644 index 0000000000..7f6cd6d5a7 --- /dev/null +++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb @@ -0,0 +1,17 @@ +class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_namespaces_lower_path ON namespaces (LOWER(path));' + execute 'CREATE INDEX CONCURRENTLY index_on_projects_lower_path ON projects (LOWER(path));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :namespaces, name: :index_on_namespaces_lower_path + remove_index :projects, name: :index_on_projects_lower_path + end +end diff --git a/db/schema.rb b/db/schema.rb index 93202f1611..c5c462c2e5 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: 20151005162154) do +ActiveRecord::Schema.define(version: 20151007120511) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb new file mode 100644 index 0000000000..0c6b533ac2 --- /dev/null +++ b/spec/benchmarks/models/project_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Project, benchmark: true do + describe '.find_with_namespace' do + let(:group) { create(:group, name: 'sisinmaru') } + let(:project) { create(:project, name: 'maru', namespace: group) } + + describe 'using a capitalized namespace' do + benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') } + + it { is_expected.to iterate_per_second(600) } + end + + describe 'using a lowercased namespace' do + benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') } + + it { is_expected.to iterate_per_second(600) } + end + end +end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 8b9f50aada..f7ed30f819 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -1,11 +1,16 @@ require 'spec_helper' describe CaseSensitivity do - describe '.case_insensitive_where' do + describe '.iwhere' do let(:connection) { ActiveRecord::Base.connection } let(:model) { Class.new { include CaseSensitivity } } describe 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + describe 'with a single column/value pair' do it 'returns the criteria for a column and a value' do criteria = double(:criteria) @@ -18,7 +23,7 @@ describe CaseSensitivity do with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + expect(model.iwhere(foo: 'bar')).to eq(criteria) end it 'returns the criteria for a column with a table, and a value' do @@ -32,7 +37,7 @@ describe CaseSensitivity do with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria) end end @@ -57,7 +62,7 @@ describe CaseSensitivity do with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + got = model.iwhere(foo: 'bar', bar: 'baz') expect(got).to eq(final) end @@ -82,7 +87,8 @@ describe CaseSensitivity do with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + got = model.iwhere(:'foo.bar' => 'bar', + :'foo.baz' => 'baz') expect(got).to eq(final) end @@ -90,6 +96,11 @@ describe CaseSensitivity do end describe 'using MySQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + end + describe 'with a single column/value pair' do it 'returns the criteria for a column and a value' do criteria = double(:criteria) @@ -99,10 +110,10 @@ describe CaseSensitivity do and_return('`foo`') expect(model).to receive(:where). - with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + with(%q{`foo` = :value}, value: 'bar'). and_return(criteria) - expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + expect(model.iwhere(foo: 'bar')).to eq(criteria) end it 'returns the criteria for a column with a table, and a value' do @@ -113,10 +124,11 @@ describe CaseSensitivity do and_return('`foo`.`bar`') expect(model).to receive(:where). - with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(criteria) - expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + expect(model.iwhere(:'foo.bar' => 'bar')). + to eq(criteria) end end @@ -134,14 +146,14 @@ describe CaseSensitivity do and_return('`bar`') expect(model).to receive(:where). - with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + with(%q{`foo` = :value}, value: 'bar'). and_return(initial) expect(initial).to receive(:where). - with(%q{LOWER(`bar`) = LOWER(:value)}, value: 'baz'). + with(%q{`bar` = :value}, value: 'baz'). and_return(final) - got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + got = model.iwhere(foo: 'bar', bar: 'baz') expect(got).to eq(final) end @@ -159,14 +171,15 @@ describe CaseSensitivity do and_return('`foo`.`baz`') expect(model).to receive(:where). - with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(initial) expect(initial).to receive(:where). - with(%q{LOWER(`foo`.`baz`) = LOWER(:value)}, value: 'baz'). + with(%q{`foo`.`baz` = :value}, value: 'baz'). and_return(final) - got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + got = model.iwhere(:'foo.bar' => 'bar', + :'foo.baz' => 'baz') expect(got).to eq(final) end From c8f18fc56283859f47ab50a5e14a7b292f8e54a5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 17:42:47 +0200 Subject: [PATCH 262/543] Added dedicated Rake task for setting up Postgres This ensures any PostgreSQL specific schema changes (e.g. expression indexes) are created when setting up the database. --- lib/tasks/gitlab/setup.rake | 1 + lib/tasks/migrate/setup_postgresql.rake | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 lib/tasks/migrate/setup_postgresql.rake diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 0ac4b0fa8a..4cbccf2ca8 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -16,6 +16,7 @@ namespace :gitlab do Rake::Task["db:setup"].invoke Rake::Task["add_limits_mysql"].invoke + Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError puts "Quitting...".red diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake new file mode 100644 index 0000000000..bf6894a835 --- /dev/null +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -0,0 +1,6 @@ +require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') + +desc 'GitLab | Sets up PostgreSQL' +task setup_postgresql: :environment do + NamespacesProjectsPathLowerIndexes.new.up +end From 2d779f70ae51d6b23fd1e5d6b14c19762ba000cc Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 18:12:15 +0200 Subject: [PATCH 263/543] Added changelog for project namespace performance --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 8ab2d9f08d..e578411c58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.1.0 (unreleased) - 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) + - 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) - Add user preference to view activities as default dashboard (Stan Hu) From 380392212f87fc80bd45c322eda54461d47745b0 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Wed, 30 Sep 2015 17:12:33 -0400 Subject: [PATCH 264/543] merge_request: coerce work_in_progress? into a boolean --- app/models/merge_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index eb468c6cd5..f69b7a13c4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -227,7 +227,7 @@ class MergeRequest < ActiveRecord::Base end def work_in_progress? - title =~ /\A\[?WIP\]?:? /i + !!(title =~ /\A\[?WIP\]?:? /i) end def mergeable? From cb13980db88c1d1ae8a5cd766ced4629c657010b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 8 Oct 2015 17:12:00 +0200 Subject: [PATCH 265/543] 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 b7def88c02b3726259800a25ee8bb82c866fe673 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 8 Oct 2015 14:59:46 -0500 Subject: [PATCH 266/543] Fix ldap email downcasing bug --- lib/gitlab/ldap/user.rb | 2 +- spec/lib/gitlab/ldap/user_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 1ea7751e27..4be99dd88c 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -35,7 +35,7 @@ module Gitlab end def find_by_email - ::User.find_by(email: auth_hash.email) + ::User.find_by(email: auth_hash.email.downcase) end def update_user_attributes diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index fd2e5f6d0e..b5b56a3495 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -13,6 +13,17 @@ describe Gitlab::LDAP::User do let(:auth_hash) do OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info) end + let(:ldap_user_upper_case) { Gitlab::LDAP::User.new(auth_hash_upper_case) } + let(:info_upper_case) do + { + name: 'John', + email: 'John@Example.com', # Email address has upper case chars + nickname: 'john' + } + end + let(:auth_hash_upper_case) do + OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case) + end describe :changed? do it "marks existing ldap user as changed" do @@ -57,6 +68,16 @@ describe Gitlab::LDAP::User do expect(existing_user.id).to eql ldap_user.gl_user.id end + it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do + existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain') + expect{ ldap_user_upper_case.save }.not_to change{ User.count } + + existing_user.reload + expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.provider).to eql 'ldapmain' + expect(existing_user.id).to eql ldap_user.gl_user.id + end + it 'maintains an identity per provider' do existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter') expect(existing_user.identities.count).to eql(1) From fce4f1380319d3436e66cc530f272e25350c3cf9 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 25 Sep 2015 13:38:12 -0500 Subject: [PATCH 267/543] Show message if user tries to fork and has no available namespaces --- app/views/projects/forks/new.html.haml | 63 ++++++++++++++------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index cd5f3a5d39..f0b0a11c04 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,36 +1,41 @@ - page_title "Fork project" -%h3.page-title Fork project -%p.lead - Click to fork the project to a user or group -%hr +- if @namespaces.present? + %h3.page-title Fork project + %p.lead + Click to fork the project to a user or group + %hr -.fork-namespaces - - @namespaces.in_groups_of(6, false) do |group| - .row - - group.each do |namespace| - .col-md-2.col-sm-3 - - if fork = namespace.find_fork_of(@project) - .fork-thumbnail - = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do - = image_tag namespace_icon(namespace, 100) - .caption - %strong - = namespace.human_name - %div.text-primary - Already forked + .fork-namespaces + - @namespaces.in_groups_of(6, false) do |group| + .row + - group.each do |namespace| + .col-md-2.col-sm-3 + - if fork = namespace.find_fork_of(@project) + .fork-thumbnail + = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 100) + .caption + %strong + = namespace.human_name + %div.text-primary + Already forked - - else - .fork-thumbnail - = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do - = image_tag namespace_icon(namespace, 100) - .caption - %strong - = namespace.human_name + - else + .fork-thumbnail + = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 100) + .caption + %strong + = namespace.human_name - %p.light - Fork is a copy of a project repository. - %br - Forking a repository allows you to do changes without affecting the original project. + %p.light + Fork is a copy of a project repository. + %br + Forking a repository allows you to do changes without affecting the original project. +- else + %h3 No available namespaces to fork the project + %p.slead + You must have permission to create a project in a namespace before forking. .save-project-loader.hide .center From 37e935943289a6fd1111cf8c2ac181fd4491a89b Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 23 Sep 2015 08:27:21 -0500 Subject: [PATCH 268/543] Clarify support request message --- PROCESS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PROCESS.md b/PROCESS.md index 1b6b3e7d32..9f4b708d2b 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -79,7 +79,11 @@ Thanks for the issue report but we only support issues for the latest stable ver ### Support requests and configuration questions -Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://about.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. +Thanks for your interest in GitLab. We don't use the issue tracker for support +requests and configuration questions. Please check our +\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available +support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) +for more information. ### Code format From 6b739027330cb07484b71c748e30d5966f63d647 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Fri, 25 Sep 2015 16:07:08 -0400 Subject: [PATCH 269/543] merge_request: add work_in_progress to MR hooks --- CHANGELOG | 1 + app/models/merge_request.rb | 3 ++- doc/web_hooks/web_hooks.md | 6 ++++-- spec/models/merge_request_spec.rb | 11 +++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 66b6e9fdf1..60d2600c49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ v 8.0.3 - Fix URL shown in Slack notifications - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) + - Add work_in_progress key to MR web hooks (Ben Boeckel) v 8.0.2 - Fix default avatar not rendering in network graph (Stan Hu) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f69b7a13c4..c83b15c7d3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -275,7 +275,8 @@ class MergeRequest < ActiveRecord::Base attrs = { source: source_project.hook_attrs, target: target_project.hook_attrs, - last_commit: nil + last_commit: nil, + work_in_progress: work_in_progress? } unless last_commit.nil? diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c185ccfcac..ef99a69f60 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -314,7 +314,8 @@ X-Gitlab-Event: Note Hook "name": "John Smith", "email": "john@example.com" } - } + }, + "work_in_progress": false } } ``` @@ -500,6 +501,7 @@ X-Gitlab-Event: Merge Request Hook "email": "gitlabdev@dv6700.(none)" } }, + "work_in_progress": false, "url": "http://example.com/diaspora/merge_requests/1", "action": "open" } @@ -537,4 +539,4 @@ When you press 'Test Hook' in GitLab, you should see something like this in the {"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",} example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 - -> / -``` \ No newline at end of file +``` diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 17a49013d2..6aaf1c036b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -165,6 +165,17 @@ describe MergeRequest do end end + describe "#hook_attrs" do + it "has all the required keys" do + attrs = subject.hook_attrs + attrs = attrs.to_h + expect(attrs).to include(:source) + expect(attrs).to include(:target) + expect(attrs).to include(:last_commit) + expect(attrs).to include(:work_in_progress) + end + end + it_behaves_like 'an editable mentionable' do subject { create(:merge_request) } From ae4fbae26cefbf10848719ee8c06d418c348420c Mon Sep 17 00:00:00 2001 From: Jonathan Rochkind Date: Thu, 8 Oct 2015 11:13:28 -0400 Subject: [PATCH 270/543] 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 6f79b5336fb9ac011dd19d5720b8bfda8b6fa53a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 18:04:16 -0400 Subject: [PATCH 271/543] Just kidding, we're deleting whenever entirely --- Gemfile | 3 --- Gemfile.lock | 4 ---- 2 files changed, 7 deletions(-) diff --git a/Gemfile b/Gemfile index e592d5ab20..e75fa37dea 100644 --- a/Gemfile +++ b/Gemfile @@ -300,9 +300,6 @@ gem 'activerecord-deprecated_finders', '~> 1.0.3' gem 'activerecord-session_store', '~> 0.1.0' gem "nested_form", '~> 0.3.2' -# Scheduled -gem 'whenever', '~> 0.9.4', require: false - # OAuth gem 'oauth2', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 29ec33bf52..896c240d59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,7 +105,6 @@ GEM celluloid (0.16.0) timers (~> 4.0.0) charlock_holmes (0.6.9.4) - chronic (0.10.2) chunky_png (1.3.4) cliver (0.3.2) coderay (1.1.0) @@ -770,8 +769,6 @@ GEM websocket-driver (0.6.2) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - whenever (0.9.4) - chronic (>= 0.6.3) wikicloth (0.8.1) builder expression_parser @@ -936,7 +933,6 @@ DEPENDENCIES version_sorter (~> 2.0.0) virtus (~> 1.0.1) webmock (~> 1.21.0) - whenever (~> 0.9.4) wikicloth (= 0.8.1) BUNDLED WITH From f4cd4086815c6ec85d4714d037a09faa903f572b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 18:12:43 -0400 Subject: [PATCH 272/543] 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 69b41ba04331e95739b25c77b3f5f198c0e64bae Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 20:34:50 -0400 Subject: [PATCH 273/543] Normalize space-like characters in keys before output to gitlab-shell gitlab-shell expects only one tab separator per key, and an SSH key with a tab character in the comment, for example, would break things. Closes #2970 --- lib/gitlab/backend/shell.rb | 3 ++- spec/lib/gitlab/backend/shell_spec.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 14ee4701e7..01b8bda05c 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -4,7 +4,8 @@ module Gitlab class KeyAdder < Struct.new(:io) def add_key(id, key) - io.puts("#{id}\t#{key.strip}") + key.gsub!(/[[:space:]]+/, ' ').strip! + io.puts("#{id}\t#{key}") end end diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index b6d0433059..b60e23454d 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -15,4 +15,17 @@ describe Gitlab::Shell do it { is_expected.to respond_to :fork_repository } it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } + + describe Gitlab::Shell::KeyAdder do + describe '#add_key' do + it 'normalizes space characters in the key' do + io = spy + adder = described_class.new(io) + + adder.add_key('key-42', "sha-rsa foo\tbar\tbaz") + + expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz") + end + end + end end From 29fe488b0cf9f8a79aad774c912be94bb737d36a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 20:53:29 -0400 Subject: [PATCH 274/543] Make the loading spinner toggle more explicit Occasionally the loading spinner would stay visible after the tab finished loading. This change makes the toggle explicit so that it's always shown on `beforeSend`, and always hidden on `complete`. Plus a bonus semi-colon eradication! :boom: --- app/assets/javascripts/merge_request_tabs.js.coffee | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 4e56791bde..3e77ea515f 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -69,7 +69,7 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash top = $(container + " " + window.location.hash).offset().top - $('body').scrollTo(top); + $('body').scrollTo(top) # Activate a tab based on the current action activateTab: (action) -> @@ -139,13 +139,16 @@ class @MergeRequestTabs @diffsLoaded = true @scrollToElement(".diffs") - toggleLoading: -> - $('.mr-loading-status .loading').toggle() + # Show or hide the loading spinner + # + # status - Boolean, true to show, false to hide + toggleLoading: (status) -> + $('.mr-loading-status .loading').toggle(status) _get: (options) -> defaults = { - beforeSend: @toggleLoading - complete: @toggleLoading + beforeSend: => @toggleLoading(true) + complete: => @toggleLoading(false) dataType: 'json' type: 'GET' } From 0c8ac9815314d91bc32eff18502788480cd62d67 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 7 Oct 2015 22:20:22 -0400 Subject: [PATCH 275/543] Remove slim --- Gemfile | 1 - Gemfile.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Gemfile b/Gemfile index 044dc30ecd..abb76b7f7d 100644 --- a/Gemfile +++ b/Gemfile @@ -128,7 +128,6 @@ gem 'after_commit_queue' gem 'acts-as-taggable-on', '~> 3.4' # Background jobs -gem 'slim', '~> 2.0.2' gem 'sinatra', '~> 1.4.4', require: nil gem 'sidekiq', '3.3.0' gem 'sidetiq', '~> 0.6.3' diff --git a/Gemfile.lock b/Gemfile.lock index f716c0254e..ac4a1f5b00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -673,9 +673,6 @@ GEM tilt (>= 1.3, < 3) six (0.2.0) slack-notifier (1.2.1) - slim (2.0.3) - temple (~> 0.6.6) - tilt (>= 1.3.3, < 2.1) slop (3.6.0) spinach (0.8.10) colorize @@ -711,7 +708,6 @@ GEM railties (>= 3.2.5, < 5) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) - temple (0.6.10) term-ansicolor (1.3.2) tins (~> 1.0) terminal-table (1.5.2) @@ -919,7 +915,6 @@ DEPENDENCIES sinatra (~> 1.4.4) six (~> 0.2.0) slack-notifier (~> 1.2.0) - slim (~> 2.0.2) spinach-rails (~> 0.2.1) spring (~> 1.3.6) spring-commands-rspec (~> 1.0.4) From 95411f7151a8e1058731a6eb5fefcf7d91312b58 Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Fri, 9 Oct 2015 14:05:55 +1100 Subject: [PATCH 276/543] fixed positioning of hamburger menu on header - Changed to margin for vertical allign as used in bootstrap 3 original - Also added an `active` state to show when the extra items are enabled --- CHANGELOG | 1 + app/assets/javascripts/application.js.coffee | 1 + app/assets/stylesheets/generic/header.scss | 7 +++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b1a35c3c2a..20bd95f03a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ v 8.1.0 (unreleased) - Move CI services page to project settings area - Add "Quick Submit" behavior to input fields throughout the application. Use Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux. + - Fix position of hamburger in header for smaller screens (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/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 8e987ac4e8..945ffb660e 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -180,6 +180,7 @@ $ -> $('.navbar-toggle').on 'click', -> $('.header-content .title').toggle() $('.header-content .navbar-collapse').toggle() + $('.navbar-toggle').toggleClass('active') # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index 543ce41ab5..91e6975e26 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -50,15 +50,17 @@ header { .navbar-toggle { color: #666; - margin: 0; + margin: 6px 0; border-radius: 0; position: absolute; right: 2px; - top: 15px; &:hover { background-color: #EEE; } + &.active { + color: #7f8fa4; + } } } } @@ -87,6 +89,7 @@ header { .navbar-collapse { float: right; + border-top: none; } } From cd46f4c36736ed548e440566095e51a3a43ca6d6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Oct 2015 23:19:56 -0400 Subject: [PATCH 277/543] Add output and confirmation to gitlab:two_factor:disable_for_all_users --- lib/tasks/gitlab/two_factor.rake | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index acd4b7da39..9196677a01 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -2,8 +2,21 @@ namespace :gitlab do namespace :two_factor do desc "GitLab | Disable Two-factor authentication (2FA) for all users" task disable_for_all_users: :environment do - User.with_two_factor.find_each do |user| - user.disable_two_factor! + scope = User.with_two_factor + count = scope.count + + if count > 0 + puts "This will disable 2FA for #{count.to_s.red} users..." + + begin + ask_to_continue + scope.find_each(&:disable_two_factor!) + puts "Successfully disabled 2FA for #{count} users.".green + rescue Gitlab::TaskAbortedByUserError + puts "Quitting...".red + end + else + puts "There are currently no users with 2FA enabled.".yellow end end end From b60bc655f2e1318f448f3a3d884437677c1135b7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 8 Oct 2015 21:24:55 -0700 Subject: [PATCH 278/543] Restore placeholders in issue filters from "Any" to "Milestone" and "Label" 1f11096c included new filters but made it hard to determine which dropdown did what. This patch restores the original placeholders. --- app/helpers/labels_helper.rb | 3 ++- app/helpers/milestones_helper.rb | 3 ++- app/models/group_milestone.rb | 2 ++ app/models/label.rb | 4 +++- app/models/milestone.rb | 6 +++++- app/views/shared/issuable/_filter.html.haml | 4 ++-- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 662ace367b..66b18eea69 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -95,7 +95,8 @@ module LabelsHelper def project_labels_options(project) labels = project.labels.to_a labels.unshift(Label::None) - options_from_collection_for_select(labels, 'name', 'name', params[:label_name]) + labels.unshift(Label::Any) + options_from_collection_for_select(labels, 'name', 'title', params[:label_name]) end # Required for Gitlab::Markdown::LabelReferenceFilter diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 132a893e53..37a5b58cce 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -30,7 +30,8 @@ module MilestonesHelper grouped_milestones = Milestones::GroupService.new(milestones).execute grouped_milestones.unshift(Milestone::None) + grouped_milestones.unshift(Milestone::Any) - options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) + options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index ab055f6b80..1dd2be68eb 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -1,5 +1,7 @@ class GroupMilestone + alias_attribute :name, :title + def initialize(title, milestones) @title = title @milestones = milestones diff --git a/app/models/label.rb b/app/models/label.rb index 14b544b375..1bb4b5f55c 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -14,7 +14,9 @@ class Label < ActiveRecord::Base include Referable # Represents a "No Label" state used for filtering Issues and Merge # Requests that have no label assigned. - None = Struct.new(:title, :name).new('No Label', 'No Label') + LabelStruct = Struct.new(:title, :name) + None = LabelStruct.new('No Label', 'No Label') + Any = LabelStruct.new('Any', '') DEFAULT_COLOR = '#428BCA' diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d979a35084..84acba30b6 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -16,7 +16,9 @@ class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - None = Struct.new(:title).new('No Milestone') + MilestoneStruct = Struct.new(:title, :name) + None = MilestoneStruct.new('No Milestone', 'No Milestone') + Any = MilestoneStruct.new('Any', '') include InternalId include Sortable @@ -47,6 +49,8 @@ class Milestone < ActiveRecord::Base state :active end + alias_attribute :name, :title + class << self def search(query) query = "%#{query}%" diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 6e6d497c1d..8f16773077 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -39,13 +39,13 @@ .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, - class: 'select2 trigger-submit', include_blank: 'Any', + 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: 'Any', + class: 'select2 trigger-submit', include_blank: true, data: {placeholder: 'Label'}) .pull-right From b5d9a613d98dc5ae629bdc004f19be7c8e5fcd8e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 8 Oct 2015 13:22:34 -0700 Subject: [PATCH 279/543] Make diff file view easier to use on mobile screens --- CHANGELOG | 1 + app/assets/stylesheets/pages/diff.scss | 114 +++++++++++------- .../stylesheets/pages/merge_requests.scss | 18 ++- app/views/projects/diffs/_file.html.haml | 2 +- 4 files changed, 90 insertions(+), 45 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b1a35c3c2a..c4f3b6693f 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) + - 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) - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5e7e59a6af..d9ef06dc6b 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,3 +1,4 @@ +// Common .diff-file { margin-left: -$gl-padding; margin-right: -$gl-padding; @@ -12,24 +13,17 @@ color: #555; z-index: 10; - > span { + .diff-title { font-family: $monospace_font; word-break: break-all; - margin-right: 200px; display: block; .file-mode { - margin-left: 10px; color: #777; } } - .diff-btn-group { - float: right; - position: absolute; - top: 5px; - right: 15px; - + .diff-controls { .btn { padding: 0px 10px; font-size: 13px; @@ -90,12 +84,12 @@ } } - tr.line_holder.parallel{ + tr.line_holder.parallel { .old_line, .new_line, .diff_line { min-width: 50px; } - td.line_content.parallel{ + td.line_content.parallel { width: 50%; } } @@ -105,7 +99,7 @@ padding: 0px; border: none; background: $background-color; - color: rgba(0,0,0,0.3); + color: rgba(0, 0, 0, 0.3); padding: 0px 5px; border-right: 1px solid $border-color; text-align: right; @@ -117,7 +111,7 @@ float: left; width: 35px; font-weight: normal; - color: rgba(0,0,0,0.3); + color: rgba(0, 0, 0, 0.3); &:hover { text-decoration: underline; } @@ -168,7 +162,7 @@ background: #ddd; text-align: center; padding: 30px; - .wrap{ + .wrap { display: inline-block; } @@ -176,7 +170,7 @@ display: inline-block; background-color: #fff; line-height: 0; - img{ + img { border: 1px solid #FFF; background: image-url('trans_bg.gif'); max-width: 100%; @@ -189,21 +183,21 @@ border: 1px solid $added; } } - .image-info{ + .image-info { font-size: 12px; margin: 5px 0 0 0; color: grey; } - .view.swipe{ + .view.swipe { position: relative; - .swipe-frame{ + .swipe-frame { display: block; margin: auto; position: relative; } - .swipe-wrap{ + .swipe-wrap { overflow: hidden; border-left: 1px solid #999; position: absolute; @@ -211,33 +205,33 @@ top: 13px; right: 7px; } - .frame{ + .frame { top: 0; right: 0; position: absolute; - &.deleted{ + &.deleted { margin: 0; display: block; top: 13px; right: 7px; } } - .swipe-bar{ + .swipe-bar { display: block; height: 100%; width: 15px; z-index: 100; position: absolute; cursor: pointer; - &:hover{ - .top-handle{ + &:hover { + .top-handle { background-position: -15px 3px; } - .bottom-handle{ + .bottom-handle { background-position: -15px -11px; } - }; - .top-handle{ + } + .top-handle { display: block; height: 14px; width: 15px; @@ -245,7 +239,7 @@ top: 0px; background: image-url('swipemode_sprites.gif') 0 3px no-repeat; } - .bottom-handle{ + .bottom-handle { display: block; height: 14px; width: 15px; @@ -254,9 +248,10 @@ background: image-url('swipemode_sprites.gif') 0 -11px no-repeat; } } - } //.view.swipe - .view.onion-skin{ - .onion-skin-frame{ + } + //.view.swipe + .view.onion-skin { + .onion-skin-frame { display: block; margin: auto; position: relative; @@ -267,7 +262,7 @@ top: 0px; left: 0px; } - .controls{ + .controls { display: block; height: 14px; width: 300px; @@ -277,7 +272,7 @@ left: 50%; margin-left: -150px; - .drag-track{ + .drag-track { display: block; position: absolute; left: 12px; @@ -317,39 +312,40 @@ background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat; } } - } //.view.onion-skin + } + //.view.onion-skin } - .view-modes{ + .view-modes { padding: 10px; text-align: center; background: #EEE; - ul, li{ + ul, li { list-style: none; margin: 0; padding: 0; display: inline-block; } - li{ + li { color: grey; border-left: 1px solid #c1c1c1; padding: 0 12px 0 16px; cursor: pointer; - &:first-child{ + &:first-child { border-left: none; } - &:hover{ + &:hover { text-decoration: underline; } - &.active{ - &:hover{ + &.active { + &:hover { text-decoration: none; } cursor: default; color: #333; } - &.disabled{ + &.disabled { display: none; } } @@ -373,3 +369,37 @@ float: right; margin-top: -5px; } + +// Mobile +@media (max-width: 480px) { + .diff-title { + margin: 0; + + .file-mode { + display: none; + } + } + + .diff-controls { + position: static; + text-align: center; + } +} + +// Bigger screens +@media (min-width: 481px) { + .diff-title { + margin-right: 200px; + + .file-mode { + margin-left: 10px; + } + } + + .diff-controls { + float: right; + position: absolute; + top: 5px; + right: 15px; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index fe69d16fc4..a1a5208c59 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -75,7 +75,7 @@ .mr-widget-footer { padding: 15px; } - + .normal { color: #5c5d5e; } @@ -102,7 +102,7 @@ } } -.merge-request .merge-request-tabs{ +.merge-request .merge-request-tabs { @include nav-menu; margin: -$gl-padding; padding: $gl-padding; @@ -110,6 +110,20 @@ margin-bottom: 1px; } +// Mobile +@media (max-width: 480px) { + .merge-request .merge-request-tabs { + margin: 0; + padding: 0; + + li { + a { + padding: 0; + } + } + } +} + .mr_source_commit, .mr_target_commit { .commit { diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 4617b18815..9698921f6d 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -16,7 +16,7 @@ - if diff_file.mode_changed? %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" - .diff-btn-group + .diff-controls - if blob.text? = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments From 972b6370819f61eb04d1f0e795b4b6c4ddc2327a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 9 Oct 2015 05:30:42 -0700 Subject: [PATCH 280/543] In SSH key detailed view, align the "Remove" button with the right container --- app/views/profiles/keys/_key_details.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index e0ae4d9720..0ca8bd9515 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -18,5 +18,6 @@ %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key - .pull-right - = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" + .col-md-12 + .pull-right + = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" From 7713a709544c86539a8060cdeed8692745ddaeca Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 9 Oct 2015 06:41:32 -0700 Subject: [PATCH 281/543] Update CHANGELOG for Emoji fix in a3c6ed5c4 [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 20bd95f03a..416ec54b13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -42,6 +42,7 @@ v 8.1.0 (unreleased) - Add "Quick Submit" behavior to input fields throughout the application. Use Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux. - Fix position of hamburger in header for smaller screens (Han Loong Liauw) + - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From 766688d0fd714ab2b42cc8e5bce2675991c73478 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 9 Oct 2015 18:11:18 -0400 Subject: [PATCH 282/543] Update monthly release post template [ci skip] --- doc/release/monthly.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c56e99a700..bd8a67d1d8 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -74,20 +74,19 @@ Xth: (1 working day before the 22nd) - [ ] Update GitLab.com with the latest RC (#LINK) - [ ] Update ci.gitLab.com with the latest RC (#LINK) -22nd before 12AM CET: +22nd before 1200 CET: -Release before 12AM CET / 3AM PST, to make sure the majority of our users +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) - [ ] Create the 'x.y.0' version on version.gitlab.com -- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) -- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK) +- [ ] 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 6PM CET / 9AM PST - +- [ ] Schedule a second tweet of the release announcement with the same text at 1800 CET / 8AM PST ``` - - - @@ -223,4 +222,4 @@ Consider creating a post on Hacker News. ## Create a WIP blogpost for the next release -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). \ No newline at end of file +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). From a972ce17caadc64fc6fa1888301e99c149848f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A7=D0=B8=D0=BD=D0=B3=D0=B8=D0=B7=20=D0=90=D1=83=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D0=BE=D0=B2?= Date: Sat, 10 Oct 2015 14:07:12 +0600 Subject: [PATCH 283/543] + and Titleize New Project button on dashboard --- app/views/dashboard/projects/_projects.html.haml | 2 +- app/views/groups/_projects.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index e09e032a7f..30372e9843 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -5,6 +5,6 @@ - if current_user.can_create_project? %span.input-group-btn = link_to new_project_path, class: 'btn btn-green' do - New project + + New Project = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2b27a88794..9c73f7a0ef 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -5,6 +5,6 @@ - if can? current_user, :create_projects, @group %span.input-group-btn = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do - New project + + New Project = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false From 78de79d779c1eb8b462fd1b5beb9f9e38bfb6f36 Mon Sep 17 00:00:00 2001 From: Jerry Lukins Date: Thu, 8 Oct 2015 07:53:05 -0400 Subject: [PATCH 284/543] Persist filters when sorting on user admin page --- app/views/admin/users/index.html.haml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index e3698ac1c4..97e4bb2fd7 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -54,19 +54,19 @@ %b.caret %ul.dropdown-menu %li - = link_to admin_users_path(sort: sort_value_name) do + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin) do + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin) do + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created) do + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created) do + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated) do + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated) do + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do = sort_title_oldest_updated = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" From 2df573dac3859034fcb90566a8ebc270a7e6088a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 9 Oct 2015 09:14:35 -0700 Subject: [PATCH 285/543] 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 c007d4a9819b1d7992259bc70ef3059955c25a65 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 10 Oct 2015 14:24:00 -0400 Subject: [PATCH 286/543] Add CHANGELOG entry for !1543 [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f9a4d17fa1..a3d796bea6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 8.1.0 (unreleased) Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux. - 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) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From 4ee8888498d7fc3921326d4929cf0b57e64411f4 Mon Sep 17 00:00:00 2001 From: Dale Noe Date: Sun, 11 Oct 2015 12:19:05 +0000 Subject: [PATCH 287/543] removed extra_sign_in_text examples for gitlab.rb [ci skip] --- doc/customization/welcome_message.md | 30 ++-------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md index 6c141d1fb7..e993230bb8 100644 --- a/doc/customization/welcome_message.md +++ b/doc/customization/welcome_message.md @@ -8,31 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab sign-in page. Users of GitLab Enterprise Edition should use the [branded login page feature](/ee/customization/branded_login_page.html) instead. -## Omnibus-gitlab example - -In `/etc/gitlab/gitlab.rb`: - -```ruby -gitlab_rails['extra_sign_in_text'] = <<'EOS' -# ACME GitLab -Welcome to the [ACME](http://www.example.com) GitLab server! -EOS -``` - -Run `sudo gitlab-ctl reconfigure` for changes to take effect. - -## Installation from source - -In `/home/git/gitlab/config/gitlab.yml`: - -```yaml -# snip -production: - # snip - extra: - sign_in_text: | - # ACME GitLab - Welcome to the [ACME](http://www.example.com) GitLab server! -``` - -Run `sudo service gitlab reload` for the change to take effect. +The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. +Admin area > Settings \ No newline at end of file From bf23ce191ba6daf3dfca604243dd88d2e3996eff Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 11 Oct 2015 18:17:49 +0200 Subject: [PATCH 288/543] Enable arbitration in MailRoom --- Gemfile | 2 +- Gemfile.lock | 4 ++-- config/mail_room.yml.example | 12 +++++++++++- doc/incoming_email/README.md | 30 ++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index e8cdb7a4a9..967092994a 100644 --- a/Gemfile +++ b/Gemfile @@ -290,7 +290,7 @@ gem 'newrelic-grape' gem 'octokit', '~> 3.7.0' -gem "mail_room", "~> 0.5.2" +gem "mail_room", "~> 0.6.0" gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index 9911904d30..58426a6068 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.5.2) + mail_room (0.6.0) 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.5.2) + mail_room (~> 0.6.0) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example index ed4a5193a6..bb624e8a18 100644 --- a/config/mail_room.yml.example +++ b/config/mail_room.yml.example @@ -12,8 +12,10 @@ # :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. @@ -25,5 +27,13 @@ # :namespace: resque:gitlab # # Always "incoming_email". # :queue: incoming_email - # # Always "EmailReceiverWorker" + # # 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 8726742347..076c8c049a 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -118,8 +118,10 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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. @@ -133,6 +135,14 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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 @@ -151,8 +161,10 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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. @@ -166,6 +178,14 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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`: @@ -228,8 +248,10 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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. @@ -243,6 +265,14 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these :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`: From 857cc388e1a93760e37145cd32514dc4026d0775 Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Mon, 12 Oct 2015 18:38:34 +1100 Subject: [PATCH 289/543] 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 914cfbd2f154ed3154a7dc3cee3309713eea786f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 6 Oct 2015 12:01:16 +0200 Subject: [PATCH 290/543] Implement Commit Status API --- CHANGELOG | 1 + app/models/ability.rb | 2 + app/models/ci/build.rb | 87 ++-------- app/models/ci/commit.rb | 88 +++++----- app/models/commit.rb | 8 + app/models/commit_status.rb | 79 +++++++++ app/models/generic_commit_status.rb | 15 ++ app/services/ci/create_commit_service.rb | 2 + app/views/projects/builds/_build.html.haml | 50 ------ app/views/projects/builds/show.html.haml | 6 +- app/views/projects/commit/ci.html.haml | 38 ++++- .../commit_statuses/_commit_status.html.haml | 52 ++++++ ...3042_add_type_and_description_to_builds.rb | 9 + ..._migrate_name_to_description_for_builds.rb | 5 + db/schema.rb | 7 +- doc/api/commits.md | 3 +- lib/api/api.rb | 1 + lib/api/commit_statuses.rb | 79 +++++++++ lib/api/entities.rb | 8 +- lib/ci/api/entities.rb | 4 +- spec/factories/ci/builds.rb | 1 + spec/factories/commit_statuses.rb | 15 ++ spec/features/commits_spec.rb | 1 + spec/models/{ci => }/build_spec.rb | 111 ------------- spec/models/ci/commit_spec.rb | 57 +++++-- .../ci/project_services/mail_service_spec.rb | 8 +- spec/models/commit_status_spec.rb | 157 ++++++++++++++++++ spec/models/generic_commit_status_spec.rb | 39 +++++ spec/requests/api/commit_status_spec.rb | 135 +++++++++++++++ spec/requests/api/commits_spec.rb | 13 ++ 30 files changed, 768 insertions(+), 313 deletions(-) create mode 100644 app/models/commit_status.rb create mode 100644 app/models/generic_commit_status.rb delete mode 100644 app/views/projects/builds/_build.html.haml create mode 100644 app/views/projects/commit_statuses/_commit_status.html.haml create mode 100644 db/migrate/20151008123042_add_type_and_description_to_builds.rb create mode 100644 db/migrate/20151008130321_migrate_name_to_description_for_builds.rb create mode 100644 lib/api/commit_statuses.rb create mode 100644 spec/factories/commit_statuses.rb rename spec/models/{ci => }/build_spec.rb (74%) create mode 100644 spec/models/commit_status_spec.rb create mode 100644 spec/models/generic_commit_status_spec.rb create mode 100644 spec/requests/api/commit_status_spec.rb diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..e38d465dd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.1.0 (unreleased) - Move CI charts to project graphs area - 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 - 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/models/ability.rb b/app/models/ability.rb index a020b24a55..77c121ca5e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -135,6 +135,8 @@ class Ability def project_report_rules project_guest_rules + [ + :create_commit_status, + :read_commit_statuses, :download_code, :fork_project, :create_project_snippet, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5d17f4418e..41ce522b2f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -24,32 +24,19 @@ # module Ci - class Build < ActiveRecord::Base - extend Ci::Model - + class Build < CommitStatus LAZY_ATTRIBUTES = ['trace'] - belongs_to :commit, class_name: 'Ci::Commit' belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' - belongs_to :user serialize :options - validates :commit, presence: true - validates :status, presence: true validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref - scope :running, ->() { where(status: "running") } - scope :pending, ->() { where(status: "pending") } - scope :success, ->() { where(status: "success") } - scope :failed, ->() { where(status: "failed") } scope :unstarted, ->() { where(runner_id: nil) } - scope :running_or_pending, ->() { where(status:[:running, :pending]) } - scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :for_ref, ->(ref) { where(ref: ref) } scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } acts_as_taggable @@ -74,13 +61,14 @@ module Ci def create_from(build) new_build = build.dup - new_build.status = :pending + new_build.status = 'pending' new_build.runner_id = nil + new_build.trigger_request_id = nil new_build.save end def retry(build) - new_build = Ci::Build.new(status: :pending) + new_build = Ci::Build.new(status: 'pending') new_build.ref = build.ref new_build.tag = build.tag new_build.options = build.options @@ -98,28 +86,7 @@ module Ci end state_machine :status, initial: :pending do - event :run do - transition pending: :running - end - - event :drop do - transition running: :failed - end - - event :success do - transition running: :success - end - - event :cancel do - transition [:pending, :running] => :canceled - end - - after_transition pending: :running do |build, transition| - build.update_attributes started_at: Time.now - end - after_transition any => [:success, :failed, :canceled] do |build, transition| - build.update_attributes finished_at: Time.now project = build.project if project.web_hooks? @@ -136,19 +103,10 @@ module Ci build.update_coverage end end - - state :pending, value: 'pending' - state :running, value: 'running' - state :failed, value: 'failed' - state :success, value: 'success' - state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :project, :gl_project, - to: :commit, prefix: false - - def before_sha - Gitlab::Git::BLANK_SHA + def ignored? + failed? && allow_failure? end def trace_html @@ -156,22 +114,6 @@ module Ci html || '' end - def started? - !pending? && !canceled? && started_at - end - - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? - end - - def ignored? - failed? && allow_failure? - end - def timeout project.timeout end @@ -180,14 +122,6 @@ module Ci yaml_variables + project_variables + trigger_variables end - def duration - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end - end - def project commit.project end @@ -278,6 +212,15 @@ module Ci "#{dir_to_trace}/#{id}.log" end + def description + name + end + + def target_url + Gitlab::Application.routes.url_helpers. + namespace_project_build_url(gl_project.namespace, gl_project, self) + end + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index fde754a92a..042a68681b 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -20,7 +20,8 @@ module Ci extend Ci::Model belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id - has_many :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :statuses, dependent: :destroy, class_name: 'CommitStatus' + has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' validates_presence_of :sha @@ -81,12 +82,11 @@ module Ci end def stage - running_or_pending = builds_without_retry.running_or_pending - running_or_pending.limit(1).pluck(:stage).first + running_or_pending = statuses.latest.running_or_pending + running_or_pending.first.try(:stage) end def create_builds(ref, tag, user, trigger_request = nil) - return if skip_ci? && trigger_request.blank? return unless config_processor config_processor.stages.any? do |stage| CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? @@ -94,7 +94,6 @@ module Ci end def create_next_builds(ref, tag, user, trigger_request) - return if skip_ci? && trigger_request.blank? return unless config_processor stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage) @@ -107,39 +106,47 @@ module Ci end def refs - builds.group(:ref).pluck(:ref) + statuses.pluck(:ref).compact.uniq end - def last_ref - builds.latest.first.try(:ref) + def statuses_for_ref(ref = nil) + if ref + statuses.for_ref(ref) + else + statuses + end end - def builds_without_retry - builds.latest + def builds_without_retry(ref = nil) + if ref + builds.for_ref(ref).latest + else + builds.latest + end end - def builds_without_retry_for_ref(ref) - builds.for_ref(ref).latest + def retried + @retried ||= (statuses.order(id: :desc) - statuses.latest) end - def retried_builds - @retried_builds ||= (builds.order(id: :desc) - builds_without_retry) - end - - def status - if skip_ci? - return 'skipped' - elsif yaml_errors.present? + def status(ref = nil) + if yaml_errors.present? return 'failed' - elsif builds.none? + end + + latest_statuses = statuses.latest.to_a + latest_statuses.reject! { |status| status.try(&:allow_failure?) } + latest_statuses.select! { |status| status.ref == nil || status.ref == ref } if ref + + if latest_statuses.none? return 'skipped' - elsif success? + elsif latest_statuses.all?(&:success?) 'success' - elsif pending? + elsif latest_statuses.all?(&:pending?) 'pending' - elsif running? + elsif latest_statuses.any?(&:running?) || latest_statuses.any?(&:pending?) 'running' - elsif canceled? + elsif latest_statuses.all?(&:canceled?) 'canceled' else 'failed' @@ -147,21 +154,15 @@ module Ci end def pending? - builds_without_retry.all? do |build| - build.pending? - end + status == 'pending' end def running? - builds_without_retry.any? do |build| - build.running? || build.pending? - end + status == 'running' end def success? - builds_without_retry.all? do |build| - build.success? || build.ignored? - end + status == 'success' end def failed? @@ -169,21 +170,15 @@ module Ci end def canceled? - builds_without_retry.all? do |build| - build.canceled? - end + status == 'canceled' end - def duration - @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i - end - - def duration_for_ref(ref) - builds_without_retry_for_ref(ref).select(&:duration).sum(&:duration).to_i + def duration(ref = nil) + statuses_for_ref(ref).latest.select(&:duration).sum(&:duration).to_i end def finished_at - @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) + @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) end def coverage @@ -195,8 +190,8 @@ module Ci end end - def matrix_for_ref?(ref) - builds_without_retry_for_ref(ref).pluck(:id).size > 1 + def matrix?(ref) + builds_without_retry(ref).pluck(:id).size > 1 end def config_processor @@ -217,7 +212,6 @@ module Ci end def skip_ci? - return false if builds.any? git_commit_message =~ /(\[ci skip\])/ if git_commit_message end diff --git a/app/models/commit.rb b/app/models/commit.rb index aff329d71f..d5c5001352 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -184,4 +184,12 @@ class Commit def parents @parents ||= Commit.decorate(super, project) end + + def ci_commit + project.ci_commit(sha) + end + + def status + ci_commit.try(:status) || :not_found + end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb new file mode 100644 index 0000000000..4c6de18527 --- /dev/null +++ b/app/models/commit_status.rb @@ -0,0 +1,79 @@ +class CommitStatus < ActiveRecord::Base + self.table_name = 'ci_builds' + + belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :user + + validates :commit, presence: true + validates :status, inclusion: {in: %w(pending running failed success canceled)} + + validates_presence_of :name + + scope :running, ->() { where(status: 'running') } + scope :pending, ->() { where(status: 'pending') } + scope :success, ->() { where(status: 'success') } + scope :failed, ->() { where(status: 'failed') } + scope :running_or_pending, ->() { where(status:[:running, :pending]) } + scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } + scope :for_ref, ->(ref) { where(ref: [ref, nil]) } + scope :running_or_pending, ->() { where(status: [:running, :pending]) } + + state_machine :status, initial: :pending do + event :run do + transition pending: :running + end + + event :drop do + transition running: :failed + end + + event :success do + transition [:pending, :running] => :success + end + + event :cancel do + transition [:pending, :running] => :canceled + end + + after_transition pending: :running do |build, transition| + build.update_attributes started_at: Time.now + end + + after_transition any => [:success, :failed, :canceled] do |build, transition| + build.update_attributes finished_at: Time.now + end + + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + end + + delegate :sha, :short_sha, :gl_project, + to: :commit, prefix: false + + def before_sha + Gitlab::Git::BLANK_SHA + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + + def duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end +end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb new file mode 100644 index 0000000000..fa54e3540d --- /dev/null +++ b/app/models/generic_commit_status.rb @@ -0,0 +1,15 @@ +class GenericCommitStatus < CommitStatus + before_validation :set_default_values + + # GitHub compatible API + alias_attribute :context, :name + + def set_default_values + self.context ||= 'default' + self.stage ||= 'external' + end + + def tags + [:external] + end +end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index fc1ae5774d..0ae3538757 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -17,6 +17,8 @@ module Ci tag = origin_ref.start_with?('refs/tags/') commit = project.gl_project.ensure_ci_commit(sha) + return false if commit.skip_ci? + commit.update_committed! commit.create_builds(ref, tag, user) diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml deleted file mode 100644 index 65fd9413b6..0000000000 --- a/app/views/projects/builds/_build.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- gl_project = build.project.gl_project -%tr.build - %td.status - = ci_status_with_icon(build.status) - - %td.build-link - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - %strong Build ##{build.id} - - - if defined?(ref) - %td - = build.ref - - %td - = build.stage - - %td - = build.name - .pull-right - - if build.tags.any? - - build.tag_list.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 - - - if build.project.coverage_enabled? - %td.coverage - - if build.coverage - #{build.coverage}% - - %td - - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) - .pull-right - - if build.active? - = link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), title: 'Cancel build' do - %i.fa.fa-remove.cred - - elsif build.commands.present? - = link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), method: :post, title: 'Retry build' do - %i.fa.fa-repeat diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b561078e8c..66e668f377 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -7,9 +7,9 @@ %code #{@build.ref} #up-build-trace - - if @commit.matrix_for_ref?(@build.ref) + - if @commit.matrix?(@build.ref) %ul.center-top-menu.build-top-menu - - @commit.builds_without_retry_for_ref(@build.ref).each do |build| + - @commit.builds_without_retry(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do = ci_icon_for_status(build.status) @@ -20,7 +20,7 @@ = build.id - - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) + - unless @commit.builds_without_retry(@build.ref).include?(@build) %li.active %a Build ##{@build.id} diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index 26ab38445c..7f106631ca 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -20,13 +20,13 @@ .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit -- @ci_commit.refs.each do |ref| +- if @ci_commit.refs.blank? .gray-content-block.second-block - Builds for #{ref} - - if @ci_commit.duration_for_ref(ref) > 0 + Latest builds + - if @ci_commit.duration > 0 %small.pull-right %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration_for_ref(ref)} + #{time_interval_in_words @ci_commit.duration} %table.table.builds %thead @@ -40,10 +40,32 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.latest, coverage: @ci_project.try(:coverage_enabled?), controls: true -- if @ci_commit.retried_builds.any? - %h3 +- @ci_commit.refs.sort.each do |ref| + .gray-content-block.second-block + Builds for #{ref} + - if @ci_commit.duration(ref) > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @ci_commit.duration(ref)} + + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %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.statuses.for_ref(ref).latest, coverage: @ci_project.try(:coverage_enabled?), controls: true + +- if @ci_commit.retried.any? + .gray-content-block.second-block Retried builds %table.table.builds @@ -59,4 +81,4 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, coverage: @ci_project.try(:coverage_enabled?), ref: true diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml new file mode 100644 index 0000000000..f79929c70b --- /dev/null +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -0,0 +1,52 @@ +%tr.commit_status + %td.status + = ci_status_with_icon(commit_status.status) + + %td.commit_status-link + - if commit_status.target_url + = link_to commit_status.target_url do + %strong Build ##{commit_status.id} + - else + %strong Build ##{commit_status.id} + + - if defined?(ref) + %td + = commit_status.ref + + %td + = commit_status.stage + + %td + = commit_status.description + .pull-right + - if commit_status.tags.any? + - commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if commit_status.try(:trigger_request) + %span.label.label-info triggered + - if commit_status.try(:allow_failure) + %span.label.label-danger allowed to fail + + %td.duration + - if commit_status.duration + #{duration_in_words(commit_status.finished_at, commit_status.started_at)} + + %td.timestamp + - if commit_status.finished_at + %span #{time_ago_in_words commit_status.finished_at} ago + + - if defined?(coverage) + %td.coverage + - if commit_status.try(:coverage) + #{commit_status.coverage}% + + %td + - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if commit_status.active? + = link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), title: 'Cancel commit_status' do + %i.fa.fa-remove.cred + - elsif commit_status.commands.present? + = link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), method: :post, title: 'Retry commit_status' do + %i.fa.fa-repeat diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb new file mode 100644 index 0000000000..c72b1c611c --- /dev/null +++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb @@ -0,0 +1,9 @@ +class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration + def change + add_column :ci_builds, :type, :string + add_column :ci_builds, :target_url, :string + add_column :ci_builds, :description, :string + add_index :ci_builds, [:commit_id, :type, :ref] + add_index :ci_builds, [:commit_id, :type, :name, :ref] + end +end diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb new file mode 100644 index 0000000000..f5c44babd8 --- /dev/null +++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb @@ -0,0 +1,5 @@ +class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration + def change + execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL") + end +end diff --git a/db/schema.rb b/db/schema.rb index c5c462c2e5..7a11dfca03 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: 20151007120511) do +ActiveRecord::Schema.define(version: 20151008130321) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -103,9 +103,14 @@ ActiveRecord::Schema.define(version: 20151007120511) do t.boolean "tag" t.string "ref" t.integer "user_id" + t.string "type" + t.string "target_url" + t.string "description" 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", "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 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 diff --git a/doc/api/commits.md b/doc/api/commits.md index eb8d6a4359..b22d040bf0 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -62,7 +62,8 @@ Parameters: "authored_date": "2012-09-20T09:06:12+03:00", "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" - ] + ], + "status": "running" } ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index c09488d354..afc0402f9e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -46,6 +46,7 @@ module API mount Services mount Files mount Commits + mount CommitStatus mount Namespaces mount Branches mount Labels diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb new file mode 100644 index 0000000000..cfe8739b17 --- /dev/null +++ b/lib/api/commit_statuses.rb @@ -0,0 +1,79 @@ +require 'mime/types' + +module API + # Project commit statuses API + class CommitStatus < Grape::API + resource :projects do + before { authenticate! } + before { authorize! :read_commit_statuses, user_project } + + # Get a commit's statuses + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # ref (optional) - The ref + # stage (optional) - The stage + # name (optional) - The name + # all (optional) - Show all statuses, default: false + # Examples: + # GET /projects/:id/repository/commits/:sha/statuses + get ':id/repository/commits/:sha/statuses' do + sha = params[:sha] + ci_commit = user_project.ci_commit(sha) + not_found! 'Commit' unless ci_commit + statuses = ci_commit.statuses + statuses = statuses.latest unless parse_boolean(params[:all]) + statuses = statuses.where(ref: params[:ref]) if params[:ref].present? + statuses = statuses.where(name: params[:stage]) if params[:stage].present? + statuses = statuses.where(name: params[:name]) if params[:name].present? + present paginate(statuses), with: Entities::CommitStatus + end + + # Post status to commit + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # ref (optional) - The ref + # state (required) - The state of the status. Can be: pending, running, success, error or failure + # target_url (optional) - The target URL to associate with this status + # description (optional) - A short description of the status + # name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default" + # Examples: + # POST /projects/:id/repository/commits/:sha/status + post ':id/statuses/:sha' do + required_attributes! [:state] + attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name] + commit = @project.commit(params[:sha]) + not_found! 'Commit' unless commit + + ci_commit = @project.ensure_ci_commit(commit.sha) + + name = params[:name] || params[:context] + status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) + status = GenericCommitStatus.new(commit: ci_commit) unless status + status.update(attrs) + + case params[:state].to_s + when 'running' + status.run + when 'success' + status.success + when 'failed' + status.drop + when 'canceled' + status.cancel + else + status.status = params[:state].to_s + end + + if status.save + present status, with: Entities::CommitStatus + else + render_validation_error!(status) + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9620d36ac4..e1c5a45975 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -65,7 +65,7 @@ module API expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda { |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count end @@ -149,6 +149,7 @@ module API class RepoCommitDetail < RepoCommit expose :parent_ids, :committed_date, :authored_date + expose :status end class ProjectSnippet < Grape::Entity @@ -228,6 +229,11 @@ module API expose :created_at end + class CommitStatus < Grape::Entity + expose :id, :sha, :ref, :status, :name, :target_url, :description, + :created_at, :started_at, :finished_at + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_type, :author_id diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index f47bc1236b..b80c0b8b27 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -2,7 +2,7 @@ module Ci module API module Entities class Commit < Grape::Entity - expose :id, :ref, :sha, :project_id, :before_sha, :created_at + expose :id, :sha, :project_id, :created_at expose :status, :finished_at, :duration expose :git_commit_message, :git_author_name, :git_author_email end @@ -12,7 +12,7 @@ module Ci end class Build < Grape::Entity - expose :id, :commands, :ref, :sha, :project_id, :repo_url, + expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url, :before_sha, :allow_git_fetch, :project_name expose :options do |model| diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 21b582afba..2fcd70182b 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -27,6 +27,7 @@ FactoryGirl.define do factory :ci_build, class: Ci::Build do + name 'test' ref 'master' tag false started_at 'Di 29. Okt 09:51:28 CET 2013' diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb new file mode 100644 index 0000000000..52de437052 --- /dev/null +++ b/spec/factories/commit_statuses.rb @@ -0,0 +1,15 @@ +FactoryGirl.define do + factory :commit_status, class: CommitStatus do + started_at 'Di 29. Okt 09:51:28 CET 2013' + finished_at 'Di 29. Okt 09:53:28 CET 2013' + name 'default' + status 'success' + description 'commit status' + commit factory: :ci_commit + + factory :generic_commit_status, class: GenericCommitStatus do + name 'generic' + description 'external commit status' + end + end +end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 5da220859e..cbb6360069 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -12,6 +12,7 @@ describe "Commits" do @ci_project = project.ensure_gitlab_ci_project @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha @build = FactoryGirl.create :ci_build, commit: @commit + @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit end before do diff --git a/spec/models/ci/build_spec.rb b/spec/models/build_spec.rb similarity index 74% rename from spec/models/ci/build_spec.rb rename to spec/models/build_spec.rb index da56f6e31a..d875015b99 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/build_spec.rb @@ -30,17 +30,9 @@ describe Ci::Build do let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } - subject { build } - it { is_expected.to belong_to(:commit) } - it { is_expected.to belong_to(:user) } - it { is_expected.to validate_presence_of :status } it { is_expected.to validate_presence_of :ref } - it { is_expected.to respond_to :success? } - it { is_expected.to respond_to :failed? } - it { is_expected.to respond_to :running? } - it { is_expected.to respond_to :pending? } it { is_expected.to respond_to :trace_html } describe :first_pending do @@ -67,72 +59,6 @@ describe Ci::Build do end end - describe :started? do - subject { build.started? } - - context 'without started_at' do - before { build.started_at = nil } - - it { is_expected.to be_falsey } - end - - %w(running success failed).each do |status| - context "if build status is #{status}" do - before { build.status = status } - - it { is_expected.to be_truthy } - end - end - - %w(pending canceled).each do |status| - context "if build status is #{status}" do - before { build.status = status } - - it { is_expected.to be_falsey } - end - end - end - - describe :active? do - subject { build.active? } - - %w(pending running).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_truthy } - end - end - - %w(success failed canceled).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_falsey } - end - end - end - - describe :complete? do - subject { build.complete? } - - %w(success failed canceled).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_truthy } - end - end - - %w(pending running).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_falsey } - end - end - end - describe :ignored? do subject { build.ignored? } @@ -200,31 +126,6 @@ describe Ci::Build do it { is_expected.to eq(commit.project.timeout) } end - describe :duration do - subject { build.duration } - - it { is_expected.to eq(120.0) } - - context 'if the building process has not started yet' do - before do - build.started_at = nil - build.finished_at = nil - end - - it { is_expected.to be_nil } - end - - context 'if the building process has started' do - before do - build.started_at = Time.now - 1.minute - build.finished_at = nil - end - - it { is_expected.to be_a(Float) } - it { is_expected.to be > 0.0 } - end - end - describe :options do let(:options) do { @@ -239,18 +140,6 @@ describe Ci::Build do it { is_expected.to eq(options) } end - describe :sha do - subject { build.sha } - - it { is_expected.to eq(commit.sha) } - end - - describe :short_sha do - subject { build.short_sha } - - it { is_expected.to eq(commit.short_sha) } - end - describe :allow_git_fetch do subject { build.allow_git_fetch } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index acff1ddf0f..371add4ee5 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -23,6 +23,8 @@ describe Ci::Commit do let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } it { is_expected.to belong_to(:gl_project) } + it { is_expected.to have_many(:statuses) } + it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } @@ -47,10 +49,12 @@ describe Ci::Commit do @second = FactoryGirl.create :ci_build, commit: commit end - it "creates new build" do + it "creates only a new build" do expect(commit.builds.count(:all)).to eq 2 + expect(commit.statuses.count(:all)).to eq 2 commit.retry expect(commit.builds.count(:all)).to eq 3 + expect(commit.statuses.count(:all)).to eq 3 end end @@ -78,8 +82,8 @@ describe Ci::Commit do subject { commit.stage } before do - @second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending - @first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending + @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending' + @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending' end it 'returns first running stage' do @@ -88,7 +92,7 @@ describe Ci::Commit do context 'first build succeeded' do before do - @first.update_attributes(status: :success) + @first.success end it 'returns last running stage' do @@ -98,8 +102,8 @@ describe Ci::Commit do context 'all builds succeeded' do before do - @first.update_attributes(status: :success) - @second.update_attributes(status: :success) + @first.success + @second.success end it 'returns nil' do @@ -111,6 +115,33 @@ describe Ci::Commit do describe :create_next_builds do end + describe :refs do + subject { commit.refs } + + before do + FactoryGirl.create :commit_status, commit: commit, name: 'deploy' + FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop' + FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master' + end + + it 'returns all refs' do + is_expected.to contain_exactly('master', 'develop') + end + end + + describe :retried do + subject { commit.retried } + + before do + @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' + @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' + end + + it 'returns old builds' do + is_expected.to contain_exactly(@commit1) + end + end + describe :create_builds do let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } @@ -252,10 +283,10 @@ describe Ci::Commit do 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 + @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 @@ -266,7 +297,7 @@ describe Ci::Commit do context 'for failed' do before do - @build4.update_attributes(status: :failed) + @build4.update_attributes(status: 'failed') end it 'to not create' do @@ -286,7 +317,7 @@ describe Ci::Commit do context 'for running' do before do - @build4.update_attributes(status: :running) + @build4.update_attributes(status: 'running') end it 'to not create' do @@ -296,7 +327,7 @@ describe Ci::Commit do context 'for retried' do before do - @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :failed + @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'failed' end it 'to not create' do diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index 04e870dce7..2ce5c2b470 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -58,7 +58,7 @@ describe Ci::MailService do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -86,7 +86,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -115,7 +115,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -144,7 +144,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb new file mode 100644 index 0000000000..c1af50afb7 --- /dev/null +++ b/spec/models/commit_status_spec.rb @@ -0,0 +1,157 @@ +require 'spec_helper' + +describe CommitStatus do + let(:commit) { FactoryGirl.create :ci_commit } + let(:commit_status) { FactoryGirl.create :commit_status, commit: commit } + + it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:user) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) } + + it { is_expected.to delegate_method(:sha).to(:commit) } + it { is_expected.to delegate_method(:short_sha).to(:commit) } + it { is_expected.to delegate_method(:gl_project).to(:commit) } + + it { is_expected.to respond_to :success? } + it { is_expected.to respond_to :failed? } + it { is_expected.to respond_to :running? } + it { is_expected.to respond_to :pending? } + + describe :started? do + subject { commit_status.started? } + + context 'without started_at' do + before { commit_status.started_at = nil } + + it { is_expected.to be_falsey } + end + + %w(running success failed).each do |status| + context "if commit status is #{status}" do + before { commit_status.status = status } + + it { is_expected.to be_truthy } + end + end + + %w(pending canceled).each do |status| + context "if commit status is #{status}" do + before { commit_status.status = status } + + it { is_expected.to be_falsey } + end + end + end + + describe :active? do + subject { commit_status.active? } + + %w(pending running).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(success failed canceled).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :complete? do + subject { commit_status.complete? } + + %w(success failed canceled).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(pending running).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :duration do + subject { commit_status.duration } + + it { is_expected.to eq(120.0) } + + context 'if the building process has not started yet' do + before do + commit_status.started_at = nil + commit_status.finished_at = nil + end + + it { is_expected.to be_nil } + end + + context 'if the building process has started' do + before do + commit_status.started_at = Time.now - 1.minute + commit_status.finished_at = nil + end + + it { is_expected.to be_a(Float) } + it { is_expected.to be > 0.0 } + end + end + + describe :latest do + subject { CommitStatus.latest.order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success' + end + + it 'return unique statuses' do + is_expected.to eq([@commit2, @commit3, @commit4, @commit5]) + end + end + + describe :for_ref do + subject { CommitStatus.for_ref('bb').order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + end + + it 'return statuses with equal and nil ref set' do + is_expected.to eq([@commit1, @commit3]) + end + end + + describe :running_or_pending do + subject { CommitStatus.running_or_pending.order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + end + + it 'return statuses that are running or pending' do + is_expected.to eq([@commit1, @commit2]) + end + end +end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb new file mode 100644 index 0000000000..f442fa5fbe --- /dev/null +++ b/spec/models/generic_commit_status_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe GenericCommitStatus do + let(:commit) { FactoryGirl.create :ci_commit } + let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit } + + describe :context do + subject { generic_commit_status.context } + before { generic_commit_status.context = 'my_context' } + + it { is_expected.to eq(generic_commit_status.name) } + end + + describe :tags do + subject { generic_commit_status.tags } + + it { is_expected.to eq([:external]) } + end + + describe :set_default_values do + before do + generic_commit_status.context = nil + generic_commit_status.stage = nil + generic_commit_status.save + end + + describe :context do + subject { generic_commit_status.context } + + it { is_expected.to_not be_nil } + end + + describe :stage do + subject { generic_commit_status.stage } + + it { is_expected.to_not be_nil } + end + end +end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb new file mode 100644 index 0000000000..d1dc97e184 --- /dev/null +++ b/spec/requests/api/commit_status_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) } + let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let(:commit) { project.repository.commit } + let!(:ci_commit) { project.ensure_ci_commit(commit.id) } + let(:commit_status) { create(:commit_status, commit: ci_commit) } + + describe "GET /projects/:id/repository/commits/:sha/statuses" do + context "reporter user" do + let(:statuses_id) { json_response.map { |status| status['id'] } } + + 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') + @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') + end + + it "should return latest commit statuses" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) + end + + it "should return all commit statuses" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id) + end + + it "should return latest commit statuses for specific ref" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status5.id) + end + + it "should return latest commit statuses for specific name" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status4.id) + end + end + + context "guest user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2) + expect(response.status).to eq(403) + end + end + + context "unauthorized user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses") + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/repository/commits/:sha/status' do + let(:post_url) { "/projects/#{project.id}/repository/commits/#{commit.id}/status" } + + context 'reporter user' do + context 'should create commit status' do + it 'with only required parameters' do + post api(post_url, user), state: 'success' + expect(response.status).to eq(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('default') + expect(json_response['ref']).to be_nil + expect(json_response['target_url']).to be_nil + expect(json_response['description']).to be_nil + end + + it 'with all optional parameters' do + post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' + expect(response.status).to eq(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['target_url']).to eq('url') + expect(json_response['description']).to eq('test') + end + end + + context 'should not create commit status' do + it 'with invalid state' do + post api(post_url, user), state: 'invalid' + expect(response.status).to eq(400) + end + + it 'without state' do + post api(post_url, user) + expect(response.status).to eq(400) + end + + it 'invalid commit' do + post api("/projects/#{project.id}/repository/commits/invalid_sha/status", user), state: 'running' + expect(response.status).to eq(404) + end + end + end + + context 'guest user' do + it 'should not create commit status' do + post api(post_url, user2) + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not create commit status' do + post api(post_url) + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a1c248c636..49acc3368f 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -47,6 +47,19 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/commits/invalid_sha", user) expect(response.status).to eq(404) end + + it "should return not_found for CI status" do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response.status).to eq(200) + expect(json_response['status']).to eq('not_found') + end + + it "should return status for CI" do + ci_commit = project.ensure_ci_commit(project.repository.commit.sha) + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response.status).to eq(200) + expect(json_response['status']).to eq(ci_commit.status) + end end context "unauthorized user" do From b0164771ec693ff58504ece560371ffec11f9ca9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 11:54:46 +0200 Subject: [PATCH 291/543] 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 d805c5dbb3c2fde0a3b97f86a2dd3abc35e72bd9 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 9 Oct 2015 19:08:37 +0300 Subject: [PATCH 292/543] Add spellcheck=false to certain input fields --- app/views/admin/users/index.html.haml | 2 +- app/views/ci/admin/runners/index.html.haml | 2 +- app/views/ci/admin/runners/show.html.haml | 2 +- app/views/dashboard/projects/_projects.html.haml | 2 +- app/views/explore/groups/index.html.haml | 2 +- app/views/explore/projects/_filter.html.haml | 2 +- app/views/groups/_projects.html.haml | 2 +- app/views/groups/group_members/index.html.haml | 2 +- app/views/layouts/_search.html.haml | 2 +- app/views/projects/project_members/index.html.haml | 2 +- app/views/search/_form.html.haml | 2 +- app/views/shared/issuable/_search_form.html.haml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index e3698ac1c4..71e15d555d 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,7 +32,7 @@ %hr = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false = hidden_field_tag "filter", params[:filter] = button_tag class: 'btn btn-primary' do %i.fa.fa-search diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index b9d6703ff4..01ce81b447 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -27,7 +27,7 @@ .pull-left = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do .form-group - = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token' + = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false = submit_tag 'Search', class: 'btn' .pull-right.light diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 5bb442cbf9..92787b2e6a 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -76,7 +76,7 @@ %td = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do .form-group - = search_field_tag :search, params[:search], class: 'form-control' + = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false = submit_tag 'Search', class: 'btn' %td diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index e09e032a7f..d0194a17b0 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1,7 +1,7 @@ .projects-list-holder .projects-search-form .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if current_user.can_create_project? %span.input-group-btn = link_to new_project_path, class: 'btn btn-green' do diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 83d4d321c8..fcb07b0408 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -11,7 +11,7 @@ = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| = hidden_field_tag :sort, @sort .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false .form-group = button_tag 'Search', class: "btn btn-default" diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 5a3d689d1e..2761272aa8 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,7 +1,7 @@ .pull-left = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false .form-group = button_tag 'Search', class: "btn btn-success" diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2b27a88794..76da3276e4 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,7 +1,7 @@ .panel.panel-default.projects-list-holder .panel-heading.clearfix .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if can? current_user, :create_projects, @group %span.input-group-btn = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 3a6d07ebdd..fee4b0052b 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -12,7 +12,7 @@ .clearfix.js-toggle-container = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } = button_tag 'Search', class: 'btn' - if current_user && current_user.can?(:admin_group_member, @group) diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index e2d2dec7ab..ceb64ce315 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control" + = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9a0a824b81..82809bec5b 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -5,7 +5,7 @@ .clearfix.js-toggle-container = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } = button_tag 'Search', class: 'btn' - if can?(current_user, :admin_project_member, @project) diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 3938c545ca..17b0981f07 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -6,7 +6,7 @@ .search-holder.clearfix .input-group - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true + = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true, spellcheck: false %span.input-group-btn = button_tag 'Search', class: "btn btn-primary" - unless params[:snippets].eql? 'true' diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index 58c3de64b7..3a5ad00aa9 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,6 +1,6 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input' } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input', spellcheck: false } = hidden_field_tag :state, params['state'] = hidden_field_tag :scope, params['scope'] = hidden_field_tag :assignee_id, params['assignee_id'] From b8f5e7427f7cbcdb19dcc5554301e87a880cfe2a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 12 Oct 2015 12:07:31 +0200 Subject: [PATCH 293/543] 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 887494761ee41e8e647753e597fab534ed738058 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 12:15:35 +0200 Subject: [PATCH 294/543] Fix commit status POST URL --- lib/api/commit_statuses.rb | 2 +- spec/requests/api/commit_status_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index cfe8739b17..41c0533429 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -41,7 +41,7 @@ module API # description (optional) - A short description of the status # name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default" # Examples: - # POST /projects/:id/repository/commits/:sha/status + # POST /projects/:id/statuses/:sha post ':id/statuses/:sha' do required_attributes! [:state] attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name] diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index d1dc97e184..b9e6dfc15a 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -72,8 +72,8 @@ describe API::API, api: true do end end - describe 'POST /projects/:id/repository/commits/:sha/status' do - let(:post_url) { "/projects/#{project.id}/repository/commits/#{commit.id}/status" } + describe 'POST /projects/:id/statuses/:sha' do + let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } context 'reporter user' do context 'should create commit status' do @@ -112,7 +112,7 @@ describe API::API, api: true do end it 'invalid commit' do - post api("/projects/#{project.id}/repository/commits/invalid_sha/status", user), state: 'running' + post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running' expect(response.status).to eq(404) end end From 7ef156a24292f98d89bc424cc245f00548831863 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 12:15:48 +0200 Subject: [PATCH 295/543] Add author to statuses --- app/models/commit_status.rb | 2 ++ lib/api/commit_statuses.rb | 2 +- lib/api/entities.rb | 1 + spec/models/commit_status_spec.rb | 7 +++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4c6de18527..de7b29a649 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -9,6 +9,8 @@ class CommitStatus < ActiveRecord::Base validates_presence_of :name + alias_attribute :author, :user + scope :running, ->() { where(status: 'running') } scope :pending, ->() { where(status: 'pending') } scope :success, ->() { where(status: 'success') } diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 41c0533429..c478b3dc24 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -52,7 +52,7 @@ module API name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) - status = GenericCommitStatus.new(commit: ci_commit) unless status + status = GenericCommitStatus.new(commit: ci_commit, user: current_user) unless status status.update(attrs) case params[:state].to_s diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e1c5a45975..bfb242bb6f 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -232,6 +232,7 @@ module API class CommitStatus < Grape::Entity expose :id, :sha, :ref, :status, :name, :target_url, :description, :created_at, :started_at, :finished_at + expose :author, using: Entities::UserBasic end class Event < Grape::Entity diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c1af50afb7..cbefa7798d 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -18,6 +18,13 @@ describe CommitStatus do it { is_expected.to respond_to :running? } it { is_expected.to respond_to :pending? } + describe :author do + subject { commit_status.author } + before { commit_status.author = User.new } + + it { is_expected.to eq(commit_status.user) } + end + describe :started? do subject { commit_status.started? } From 07f60552728db7276ad24dafd1ff601ae49442d2 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 9 Oct 2015 13:20:38 +0300 Subject: [PATCH 296/543] Invalidate stored service password if the endpoint URL is changed --- app/models/project_services/bamboo_service.rb | 7 +++ .../project_services/teamcity_service.rb | 7 +++ app/models/service.rb | 9 +++ .../project_services/bamboo_service_spec.rb | 56 +++++++++++++++++++ .../project_services/teamcity_service_spec.rb | 56 +++++++++++++++++++ spec/models/service_spec.rb | 23 ++++++++ 6 files changed, 158 insertions(+) create mode 100644 spec/models/project_services/bamboo_service_spec.rb create mode 100644 spec/models/project_services/teamcity_service_spec.rb diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index d8aedbd2ab..5f5255ab48 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -40,12 +40,19 @@ class BambooService < CiService attr_accessor :response after_save :compose_service_hook, if: :activated? + before_update :reset_password def compose_service_hook hook = service_hook || build_service_hook hook.save end + def reset_password + if prop_updated?(:bamboo_url) + self.password = nil + end + end + def title 'Atlassian Bamboo CI' end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 3c002a1634..fb11cad352 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -37,12 +37,19 @@ class TeamcityService < CiService attr_accessor :response after_save :compose_service_hook, if: :activated? + before_update :reset_password def compose_service_hook hook = service_hook || build_service_hook hook.save end + def reset_password + if prop_updated?(:teamcity_url) + self.password = nil + end + end + def title 'JetBrains TeamCity CI' end diff --git a/app/models/service.rb b/app/models/service.rb index 60fcc9d285..7e845d565b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -117,6 +117,15 @@ class Service < ActiveRecord::Base 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) + 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 new file mode 100644 index 0000000000..f8a3493f52 --- /dev/null +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -0,0 +1,56 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe BambooService, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" 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 + + 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 + end +end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb new file mode 100644 index 0000000000..3dbd2346bc --- /dev/null +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -0,0 +1,56 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe TeamcityService, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" 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 + + 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 + end +end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index a213ffe6c4..da87ea5b84 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -103,4 +103,27 @@ describe Service do end end end + + describe "#prop_updated?" do + let(:service) do + BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false" 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" + expect(service.prop_updated?(:bamboo_url)).to be_truthy + end + end end From 0aefeeb882b40d740b80f15ac6610a88a340d30b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 12:16:00 +0200 Subject: [PATCH 297/543] Add Commit Status documentation --- app/models/ci/commit.rb | 2 +- app/models/commit_status.rb | 2 +- doc/api/commits.md | 82 +++++++++++++++++++++++++++++++++++++ lib/api/commit_statuses.rb | 20 ++++----- 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 042a68681b..623ff619c4 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -136,7 +136,7 @@ module Ci latest_statuses = statuses.latest.to_a latest_statuses.reject! { |status| status.try(&:allow_failure?) } - latest_statuses.select! { |status| status.ref == nil || status.ref == ref } if ref + latest_statuses.select! { |status| status.ref.nil? || status.ref == ref } if ref if latest_statuses.none? return 'skipped' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index de7b29a649..a4896a7631 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base belongs_to :user validates :commit, presence: true - validates :status, inclusion: {in: %w(pending running failed success canceled)} + validates :status, inclusion: { in: %w(pending running failed success canceled) } validates_presence_of :name diff --git a/doc/api/commits.md b/doc/api/commits.md index b22d040bf0..78144dd42e 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -157,3 +157,85 @@ Parameters: "line_type": "new" } ``` + +## Get the status of a commit + +Get the statuses of a commit in a project. + +``` +GET /projects/:id/repository/commits/:sha/statuses +``` + +Parameters: + +- `id` (required) - The ID of a project +- `sha` (required) - The commit SHA +- `ref` (optional) - Filter by ref name, it can be branch or tag +- `stage` (optional) - Filter by stage +- `name` (optional) - Filer by status name, eg. jenkins +- `all` (optional) - The flag to return all statuses, not only latest ones + +```json +[ + { + "id": 13, + "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", + "ref": "test", + "status": "success", + "name": "ci/jenkins", + "target_url": "http://jenkins/project/url", + "description": "Jenkins success", + "created_at": "2015-10-12T09:47:16.250Z", + "started_at": "2015-10-12T09:47:16.250Z"", + "finished_at": "2015-10-12T09:47:16.262Z", + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + } + } +] +``` + +## Post the status to commit + +Adds or updates a status of a commit. +Optionally you can post comments on a specific line of a commit. Therefor both `path`, `line_new` and `line_old` are required. + +``` +POST /projects/:id/statuses/:sha +``` + +- `id` (required) - The ID of a project +- `sha` (required) - The commit SHA +- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled +- `ref` (optional) - The ref (branch or tag) to which the status refers +- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default" +- `target_url` (optional) - The target URL to associate with this status +- `description` (optional) - The short description of the status + +```json +{ + "id": 13, + "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", + "ref": "test", + "status": "success", + "name": "ci/jenkins", + "target_url": "http://jenkins/project/url", + "description": "Jenkins success", + "created_at": "2015-10-12T09:47:16.250Z", + "started_at": "2015-10-12T09:47:16.250Z"", + "finished_at": "2015-10-12T09:47:16.262Z", + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + } +} +``` diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index c478b3dc24..ca750320e4 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -56,16 +56,16 @@ module API status.update(attrs) case params[:state].to_s - when 'running' - status.run - when 'success' - status.success - when 'failed' - status.drop - when 'canceled' - status.cancel - else - status.status = params[:state].to_s + when 'running' + status.run + when 'success' + status.success + when 'failed' + status.drop + when 'canceled' + status.cancel + else + status.status = params[:state].to_s end if status.save From 27d952b1197f2dc615c383c21eb287313d81c74c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 14:30:44 +0200 Subject: [PATCH 298/543] 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 c2c8f8acc41747280356e157e749c1cafbd807e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 14:36:06 +0200 Subject: [PATCH 299/543] Remove warning about Reply by email and HA --- doc/incoming_email/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 076c8c049a..aafa2345fa 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -2,10 +2,6 @@ GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. -**Warning**: Do not enable Reply by email if you have **multiple GitLab application servers**. -Due to an issue with the way incoming emails are read from the mail server, every incoming reply-by-email email will result in as many comments being created as you have application servers. -[A fix is being worked on.](https://github.com/tpitale/mail_room/issues/46) - ## 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. From d6a523d78569020a00cf707f34a1824996261037 Mon Sep 17 00:00:00 2001 From: teuneboon Date: Mon, 12 Oct 2015 14:37:07 +0200 Subject: [PATCH 300/543] Fix typo(missing quote) when deleting groups --- app/controllers/groups_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 524218290c..735de309fd 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -88,7 +88,7 @@ class GroupsController < Groups::ApplicationController def destroy DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, alert: "Group '#{@group.name} was deleted." + redirect_to root_path, alert: "Group '#{@group.name}' was deleted." end protected From 5479797f0a3a648824fd98d17360bbf886957bf1 Mon Sep 17 00:00:00 2001 From: teuneboon Date: Mon, 12 Oct 2015 14:37:56 +0200 Subject: [PATCH 301/543] Make group deleted text consistent with group created and updated texts --- app/controllers/groups_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 735de309fd..40fb15a5b3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -88,7 +88,7 @@ class GroupsController < Groups::ApplicationController def destroy DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, alert: "Group '#{@group.name}' was deleted." + redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted." end protected From 41075bad8daeed07bceaa44cbfe54ab1b470ca87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 14:39:33 +0200 Subject: [PATCH 302/543] 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 cba6d62323eba2286f78e8aea12a5ecd26903728 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 12 Oct 2015 14:49:23 +0200 Subject: [PATCH 303/543] Move CI styles to pages dir Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/{ci => pages}/builds.scss | 0 .../stylesheets/{ci/projects.scss => pages/ci_projects.scss} | 0 app/assets/stylesheets/{ci => pages}/lint.scss | 0 app/assets/stylesheets/{ci => pages}/runners.scss | 0 app/assets/stylesheets/{ci => pages}/status.scss | 0 app/assets/stylesheets/{ci => pages}/xterm.scss | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename app/assets/stylesheets/{ci => pages}/builds.scss (100%) rename app/assets/stylesheets/{ci/projects.scss => pages/ci_projects.scss} (100%) rename app/assets/stylesheets/{ci => pages}/lint.scss (100%) rename app/assets/stylesheets/{ci => pages}/runners.scss (100%) rename app/assets/stylesheets/{ci => pages}/status.scss (100%) rename app/assets/stylesheets/{ci => pages}/xterm.scss (100%) diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/pages/builds.scss similarity index 100% rename from app/assets/stylesheets/ci/builds.scss rename to app/assets/stylesheets/pages/builds.scss diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/pages/ci_projects.scss similarity index 100% rename from app/assets/stylesheets/ci/projects.scss rename to app/assets/stylesheets/pages/ci_projects.scss diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/pages/lint.scss similarity index 100% rename from app/assets/stylesheets/ci/lint.scss rename to app/assets/stylesheets/pages/lint.scss diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/pages/runners.scss similarity index 100% rename from app/assets/stylesheets/ci/runners.scss rename to app/assets/stylesheets/pages/runners.scss diff --git a/app/assets/stylesheets/ci/status.scss b/app/assets/stylesheets/pages/status.scss similarity index 100% rename from app/assets/stylesheets/ci/status.scss rename to app/assets/stylesheets/pages/status.scss diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/pages/xterm.scss similarity index 100% rename from app/assets/stylesheets/ci/xterm.scss rename to app/assets/stylesheets/pages/xterm.scss From bdd477a0f800328a527f2fad92d1303441689341 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 12 Oct 2015 15:00:04 +0200 Subject: [PATCH 304/543] Re-organize GitLab css into 2 directories: framework and page specific css Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/application.scss | 30 ++--------------- app/assets/stylesheets/framework.scss | 32 +++++++++++++++++++ .../{generic => framework}/avatar.scss | 0 .../{generic => framework}/blocks.scss | 0 .../{generic => framework}/buttons.scss | 0 .../{generic => framework}/calendar.scss | 0 .../{generic => framework}/callout.scss | 0 .../{generic => framework}/common.scss | 0 .../{generic => framework}/files.scss | 0 .../{generic => framework}/filters.scss | 0 .../{generic => framework}/flash.scss | 0 .../{base => framework}/fonts.scss | 0 .../{generic => framework}/forms.scss | 0 .../{generic => framework}/gfm.scss | 0 .../{themes => framework}/gitlab-theme.scss | 0 .../{base => framework}/gl_bootstrap.scss | 0 .../{base => framework}/gl_variables.scss | 0 .../{generic => framework}/header.scss | 0 .../{generic => framework}/highlight.scss | 0 .../{generic => framework}/issue_box.scss | 0 .../{generic => framework}/jquery.scss | 0 .../{base => framework}/layout.scss | 0 .../{generic => framework}/lists.scss | 0 .../{generic => framework}/markdown_area.scss | 0 .../{base => framework}/mixins.scss | 0 .../{generic => framework}/mobile.scss | 0 .../{generic => framework}/pagination.scss | 0 .../{generic => framework}/selects.scss | 0 .../{generic => framework}/sidebar.scss | 0 .../{generic => framework}/tables.scss | 0 .../{generic => framework}/timeline.scss | 0 .../{generic => framework}/typography.scss | 0 .../{base => framework}/variables.scss | 0 .../{generic => framework}/zen.scss | 0 34 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 app/assets/stylesheets/framework.scss rename app/assets/stylesheets/{generic => framework}/avatar.scss (100%) rename app/assets/stylesheets/{generic => framework}/blocks.scss (100%) rename app/assets/stylesheets/{generic => framework}/buttons.scss (100%) rename app/assets/stylesheets/{generic => framework}/calendar.scss (100%) rename app/assets/stylesheets/{generic => framework}/callout.scss (100%) rename app/assets/stylesheets/{generic => framework}/common.scss (100%) rename app/assets/stylesheets/{generic => framework}/files.scss (100%) rename app/assets/stylesheets/{generic => framework}/filters.scss (100%) rename app/assets/stylesheets/{generic => framework}/flash.scss (100%) rename app/assets/stylesheets/{base => framework}/fonts.scss (100%) rename app/assets/stylesheets/{generic => framework}/forms.scss (100%) rename app/assets/stylesheets/{generic => framework}/gfm.scss (100%) rename app/assets/stylesheets/{themes => framework}/gitlab-theme.scss (100%) rename app/assets/stylesheets/{base => framework}/gl_bootstrap.scss (100%) rename app/assets/stylesheets/{base => framework}/gl_variables.scss (100%) rename app/assets/stylesheets/{generic => framework}/header.scss (100%) rename app/assets/stylesheets/{generic => framework}/highlight.scss (100%) rename app/assets/stylesheets/{generic => framework}/issue_box.scss (100%) rename app/assets/stylesheets/{generic => framework}/jquery.scss (100%) rename app/assets/stylesheets/{base => framework}/layout.scss (100%) rename app/assets/stylesheets/{generic => framework}/lists.scss (100%) rename app/assets/stylesheets/{generic => framework}/markdown_area.scss (100%) rename app/assets/stylesheets/{base => framework}/mixins.scss (100%) rename app/assets/stylesheets/{generic => framework}/mobile.scss (100%) rename app/assets/stylesheets/{generic => framework}/pagination.scss (100%) rename app/assets/stylesheets/{generic => framework}/selects.scss (100%) rename app/assets/stylesheets/{generic => framework}/sidebar.scss (100%) rename app/assets/stylesheets/{generic => framework}/tables.scss (100%) rename app/assets/stylesheets/{generic => framework}/timeline.scss (100%) rename app/assets/stylesheets/{generic => framework}/typography.scss (100%) rename app/assets/stylesheets/{base => framework}/variables.scss (100%) rename app/assets/stylesheets/{generic => framework}/zen.scss (100%) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d9ede63794..233e01cc06 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,18 +11,10 @@ *= require cal-heatmap */ - -@import "base/fonts"; -@import "base/variables"; -@import "base/mixins"; -@import "base/layout"; - - /** - * Customized Twitter bootstrap + * GitLab UI framework */ -@import 'base/gl_variables'; -@import 'base/gl_bootstrap'; +@import "framework"; /** * NProgress load bar css @@ -32,24 +24,12 @@ /** * Font icons - * */ @import "font-awesome"; -/** - * UI themes: - */ -@import "themes/**/*"; - -/** - * Generic css (forms, nav etc): - */ -@import "generic/**/*"; - /** * Page specific styles (issues, projects etc): */ - @import "pages/**/*"; /** @@ -61,9 +41,3 @@ * Styles for JS behaviors. */ @import "behaviors.scss"; - -/** - * CI specific styles: - */ -@import "ci/**/*"; - diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss new file mode 100644 index 0000000000..c5e23c1c32 --- /dev/null +++ b/app/assets/stylesheets/framework.scss @@ -0,0 +1,32 @@ +@import "framework/fonts"; +@import "framework/variables"; +@import "framework/mixins"; +@import "framework/layout"; +@import 'framework/gl_variables'; +@import 'framework/gl_bootstrap'; +@import "framework/avatar.scss"; +@import "framework/blocks.scss"; +@import "framework/buttons.scss"; +@import "framework/calendar.scss"; +@import "framework/callout.scss"; +@import "framework/common.scss"; +@import "framework/files.scss"; +@import "framework/filters.scss"; +@import "framework/flash.scss"; +@import "framework/forms.scss"; +@import "framework/gfm.scss"; +@import "framework/gitlab-theme.scss"; +@import "framework/header.scss"; +@import "framework/highlight.scss"; +@import "framework/issue_box.scss"; +@import "framework/jquery.scss"; +@import "framework/lists.scss"; +@import "framework/markdown_area.scss"; +@import "framework/mobile.scss"; +@import "framework/pagination.scss"; +@import "framework/selects.scss"; +@import "framework/sidebar.scss"; +@import "framework/tables.scss"; +@import "framework/timeline.scss"; +@import "framework/typography.scss"; +@import "framework/zen.scss"; diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/framework/avatar.scss similarity index 100% rename from app/assets/stylesheets/generic/avatar.scss rename to app/assets/stylesheets/framework/avatar.scss diff --git a/app/assets/stylesheets/generic/blocks.scss b/app/assets/stylesheets/framework/blocks.scss similarity index 100% rename from app/assets/stylesheets/generic/blocks.scss rename to app/assets/stylesheets/framework/blocks.scss diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/framework/buttons.scss similarity index 100% rename from app/assets/stylesheets/generic/buttons.scss rename to app/assets/stylesheets/framework/buttons.scss diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/framework/calendar.scss similarity index 100% rename from app/assets/stylesheets/generic/calendar.scss rename to app/assets/stylesheets/framework/calendar.scss diff --git a/app/assets/stylesheets/generic/callout.scss b/app/assets/stylesheets/framework/callout.scss similarity index 100% rename from app/assets/stylesheets/generic/callout.scss rename to app/assets/stylesheets/framework/callout.scss diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/framework/common.scss similarity index 100% rename from app/assets/stylesheets/generic/common.scss rename to app/assets/stylesheets/framework/common.scss diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/framework/files.scss similarity index 100% rename from app/assets/stylesheets/generic/files.scss rename to app/assets/stylesheets/framework/files.scss diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/framework/filters.scss similarity index 100% rename from app/assets/stylesheets/generic/filters.scss rename to app/assets/stylesheets/framework/filters.scss diff --git a/app/assets/stylesheets/generic/flash.scss b/app/assets/stylesheets/framework/flash.scss similarity index 100% rename from app/assets/stylesheets/generic/flash.scss rename to app/assets/stylesheets/framework/flash.scss diff --git a/app/assets/stylesheets/base/fonts.scss b/app/assets/stylesheets/framework/fonts.scss similarity index 100% rename from app/assets/stylesheets/base/fonts.scss rename to app/assets/stylesheets/framework/fonts.scss diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/framework/forms.scss similarity index 100% rename from app/assets/stylesheets/generic/forms.scss rename to app/assets/stylesheets/framework/forms.scss diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/framework/gfm.scss similarity index 100% rename from app/assets/stylesheets/generic/gfm.scss rename to app/assets/stylesheets/framework/gfm.scss diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss similarity index 100% rename from app/assets/stylesheets/themes/gitlab-theme.scss rename to app/assets/stylesheets/framework/gitlab-theme.scss diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/framework/gl_bootstrap.scss similarity index 100% rename from app/assets/stylesheets/base/gl_bootstrap.scss rename to app/assets/stylesheets/framework/gl_bootstrap.scss diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/framework/gl_variables.scss similarity index 100% rename from app/assets/stylesheets/base/gl_variables.scss rename to app/assets/stylesheets/framework/gl_variables.scss diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/framework/header.scss similarity index 100% rename from app/assets/stylesheets/generic/header.scss rename to app/assets/stylesheets/framework/header.scss diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/framework/highlight.scss similarity index 100% rename from app/assets/stylesheets/generic/highlight.scss rename to app/assets/stylesheets/framework/highlight.scss diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss similarity index 100% rename from app/assets/stylesheets/generic/issue_box.scss rename to app/assets/stylesheets/framework/issue_box.scss diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/framework/jquery.scss similarity index 100% rename from app/assets/stylesheets/generic/jquery.scss rename to app/assets/stylesheets/framework/jquery.scss diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/framework/layout.scss similarity index 100% rename from app/assets/stylesheets/base/layout.scss rename to app/assets/stylesheets/framework/layout.scss diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/framework/lists.scss similarity index 100% rename from app/assets/stylesheets/generic/lists.scss rename to app/assets/stylesheets/framework/lists.scss diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss similarity index 100% rename from app/assets/stylesheets/generic/markdown_area.scss rename to app/assets/stylesheets/framework/markdown_area.scss diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/framework/mixins.scss similarity index 100% rename from app/assets/stylesheets/base/mixins.scss rename to app/assets/stylesheets/framework/mixins.scss diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/framework/mobile.scss similarity index 100% rename from app/assets/stylesheets/generic/mobile.scss rename to app/assets/stylesheets/framework/mobile.scss diff --git a/app/assets/stylesheets/generic/pagination.scss b/app/assets/stylesheets/framework/pagination.scss similarity index 100% rename from app/assets/stylesheets/generic/pagination.scss rename to app/assets/stylesheets/framework/pagination.scss diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/framework/selects.scss similarity index 100% rename from app/assets/stylesheets/generic/selects.scss rename to app/assets/stylesheets/framework/selects.scss diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss similarity index 100% rename from app/assets/stylesheets/generic/sidebar.scss rename to app/assets/stylesheets/framework/sidebar.scss diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/framework/tables.scss similarity index 100% rename from app/assets/stylesheets/generic/tables.scss rename to app/assets/stylesheets/framework/tables.scss diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/framework/timeline.scss similarity index 100% rename from app/assets/stylesheets/generic/timeline.scss rename to app/assets/stylesheets/framework/timeline.scss diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/framework/typography.scss similarity index 100% rename from app/assets/stylesheets/generic/typography.scss rename to app/assets/stylesheets/framework/typography.scss diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/framework/variables.scss similarity index 100% rename from app/assets/stylesheets/base/variables.scss rename to app/assets/stylesheets/framework/variables.scss diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/framework/zen.scss similarity index 100% rename from app/assets/stylesheets/generic/zen.scss rename to app/assets/stylesheets/framework/zen.scss From c42d35b7214348e5c31014b3d3d54e59f6030ddc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 12 Oct 2015 15:08:33 +0200 Subject: [PATCH 305/543] Add css welcome notice Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/application.scss | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 233e01cc06..65f775ca3f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,33 +11,41 @@ *= require cal-heatmap */ -/** +/* + * Welcome to GitLab css! + * If you need to add or modify UI component that is commont for many pages + * like table or typography then make changes in framework/ directory. + * If you need to add uniq style that should affect only one page - use pages/ + * directory. + */ + +/* * GitLab UI framework */ @import "framework"; -/** +/* * NProgress load bar css */ @import 'nprogress'; @import 'nprogress-bootstrap'; -/** +/* * Font icons */ @import "font-awesome"; -/** +/* * Page specific styles (issues, projects etc): */ @import "pages/**/*"; -/** +/* * Code highlight */ @import "highlight/**/*"; -/** +/* * Styles for JS behaviors. */ @import "behaviors.scss"; From 024e34e94d973842cf02d9177e9ec52bd587ceee Mon Sep 17 00:00:00 2001 From: Alex Lossent Date: Mon, 12 Oct 2015 15:24:00 +0200 Subject: [PATCH 306/543] Hide passwords to non-admin users in the services API In order to be consistent with !1490 doing it for the web interface --- CHANGELOG | 1 + lib/api/entities.rb | 12 +++++++++ lib/api/services.rb | 2 +- spec/requests/api/services_spec.rb | 33 ++++++++++++++++++++++++- spec/support/services_shared_context.rb | 8 +++++- 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..10ea52a12f 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) + - Hide passwords from services API (Alex Lossent) v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9620d36ac4..7a1e702c75 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -255,6 +255,18 @@ module API expose :notification_level end + class ProjectService < Grape::Entity + expose :id, :title, :created_at, :updated_at, :active + expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events + # Expose serialized properties + expose :properties do |service, options| + field_names = service.fields. + select { |field| options[:include_passwords] || field[:type] != 'password' }. + map { |field| field[:name] } + service.properties.slice(*field_names) + end + end + class ProjectWithAccess < Project expose :permissions do expose :project_access, using: Entities::ProjectAccess do |project, options| diff --git a/lib/api/services.rb b/lib/api/services.rb index 6727e80ac1..203f04a625 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -57,7 +57,7 @@ module API # GET /project/:id/services/gitlab-ci # get ':id/services/:service_slug' do - present project_service + present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 9aa60826f2..c0226605a2 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:user2) { create(:user) } let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } Service.available_services_names.each do |service| @@ -51,11 +53,40 @@ describe API::API, api: true do describe "GET /projects/:id/services/#{service.dasherize}" do include_context service - it "should get #{service} settings" do + # inject some properties into the service + before do + project.build_missing_services + service_object = project.send(service_method) + service_object.properties = service_attrs + service_object.save + end + + it 'should return authentication error when unauthenticated' do + get api("/projects/#{project.id}/services/#{dashed_service}") + expect(response.status).to eq(401) + end + + it "should return all properties of service #{service} when authenticated as admin" do + get api("/projects/#{project.id}/services/#{dashed_service}", admin) + + expect(response.status).to eq(200) + expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map) + end + + it "should return properties of service #{service} other than passwords when authenticated as project owner" do get api("/projects/#{project.id}/services/#{dashed_service}", user) expect(response.status).to eq(200) + expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords) end + + it "should return error when authenticated but not a project owner" do + project.team << [user2, :developer] + get api("/projects/#{project.id}/services/#{dashed_service}", user2) + + expect(response.status).to eq(403) + end + end end end diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 4d007ae55e..d1c999cad4 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -3,7 +3,13 @@ Service.available_services_names.each do |service| let(:dashed_service) { service.dasherize } let(:service_method) { "#{service}_service".to_sym } let(:service_klass) { "#{service}_service".classify.constantize } - let(:service_attrs_list) { service_klass.new.fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } + let(:service_fields) { service_klass.new.fields } + let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } + let(:service_attrs_list_without_passwords) do + service_fields. + select { |field| field[:type] != 'password' }. + map { |field| field[:name].to_sym} + end let(:service_attrs) do service_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/ From 7a0cc665ff5ad3969f36082baa162a2169c34612 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 15:34:08 +0200 Subject: [PATCH 307/543] 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 69c04498ef0a49186a667fd44383b9b43690a91e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 15:45:46 +0200 Subject: [PATCH 308/543] Small bug fixes --- app/models/ci/build.rb | 16 +++++++++---- app/models/commit_status.rb | 23 +++++++++++++------ .../commit_statuses/_commit_status.html.haml | 16 ++++++------- doc/api/commits.md | 1 - lib/api/commit_statuses.rb | 5 ++-- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 41ce522b2f..cb6a101521 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -212,15 +212,23 @@ module Ci "#{dir_to_trace}/#{id}.log" end - def description - name - end - def target_url Gitlab::Application.routes.url_helpers. namespace_project_build_url(gl_project.namespace, gl_project, self) end + def cancel_url + if active? + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + end + end + + def retry_url + if commands.present? + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + end + end + private def yaml_variables diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a4896a7631..b6234c896e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -11,14 +11,14 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user - scope :running, ->() { where(status: 'running') } - scope :pending, ->() { where(status: 'pending') } - scope :success, ->() { where(status: 'success') } - scope :failed, ->() { where(status: 'failed') } - scope :running_or_pending, ->() { where(status:[:running, :pending]) } - scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } + scope :running, -> { where(status: 'running') } + scope :pending, -> { where(status: 'pending') } + scope :success, -> { where(status: 'success') } + scope :failed, -> { where(status: 'failed') } + scope :running_or_pending, -> { where(status:[:running, :pending]) } + scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } scope :for_ref, ->(ref) { where(ref: [ref, nil]) } - scope :running_or_pending, ->() { where(status: [:running, :pending]) } + scope :running_or_pending, -> { where(status: [:running, :pending]) } state_machine :status, initial: :pending do event :run do @@ -55,6 +55,7 @@ class CommitStatus < ActiveRecord::Base delegate :sha, :short_sha, :gl_project, to: :commit, prefix: false + # TODO: this should be removed with all references def before_sha Gitlab::Git::BLANK_SHA end @@ -78,4 +79,12 @@ class CommitStatus < ActiveRecord::Base Time.now - started_at end end + + def cancel_url + nil + end + + def retry_url + nil + end end diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index f79929c70b..14b814bb0d 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -9,7 +9,7 @@ - else %strong Build ##{commit_status.id} - - if defined?(ref) + - if defined?(ref) && ref %td = commit_status.ref @@ -17,7 +17,7 @@ = commit_status.stage %td - = commit_status.description + = commit_status.name .pull-right - if commit_status.tags.any? - commit_status.tags.each do |tag| @@ -36,17 +36,17 @@ - if commit_status.finished_at %span #{time_ago_in_words commit_status.finished_at} ago - - if defined?(coverage) + - if defined?(coverage) && coverage %td.coverage - if commit_status.try(:coverage) #{commit_status.coverage}% %td - - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) + - if defined?(controls) && controls && current_user && can?(current_user, :manage_builds, gl_project) .pull-right - - if commit_status.active? - = link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), title: 'Cancel commit_status' do + - if commit_status.cancel_url + = link_to commit_status.cancel_url, title: 'Cancel' do %i.fa.fa-remove.cred - - elsif commit_status.commands.present? - = link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), method: :post, title: 'Retry commit_status' do + - elsif commit_status.retry_url + = link_to commit_status.retry_url, method: :post, title: 'Retry' do %i.fa.fa-repeat diff --git a/doc/api/commits.md b/doc/api/commits.md index 78144dd42e..9f72adc6ed 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -203,7 +203,6 @@ Parameters: ## Post the status to commit Adds or updates a status of a commit. -Optionally you can post comments on a specific line of a commit. Therefor both `path`, `line_new` and `line_old` are required. ``` POST /projects/:id/statuses/:sha diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index ca750320e4..2a005d6a9f 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -5,7 +5,6 @@ module API class CommitStatus < Grape::API resource :projects do before { authenticate! } - before { authorize! :read_commit_statuses, user_project } # Get a commit's statuses # @@ -19,13 +18,14 @@ module API # Examples: # GET /projects/:id/repository/commits/:sha/statuses get ':id/repository/commits/:sha/statuses' do + authorize! :read_commit_statuses, user_project sha = params[:sha] ci_commit = user_project.ci_commit(sha) not_found! 'Commit' unless ci_commit statuses = ci_commit.statuses statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? - statuses = statuses.where(name: params[:stage]) if params[:stage].present? + statuses = statuses.where(stage: params[:stage]) if params[:stage].present? statuses = statuses.where(name: params[:name]) if params[:name].present? present paginate(statuses), with: Entities::CommitStatus end @@ -43,6 +43,7 @@ module API # Examples: # POST /projects/:id/statuses/:sha post ':id/statuses/:sha' do + authorize! :create_commit_statuses, user_project required_attributes! [:state] attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name] commit = @project.commit(params[:sha]) From 6f35614852a7cf360bbc4af6ba6c450d8667c04e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 12 Oct 2015 16:23:15 +0200 Subject: [PATCH 309/543] 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 789fe7b4899fb97c48ad363c5ecb1969d78d1536 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 16:32:58 +0200 Subject: [PATCH 310/543] Update rendering --- app/models/ci/commit.rb | 64 +++++++++--------- app/models/commit_status.rb | 5 +- app/views/projects/builds/show.html.haml | 6 +- app/views/projects/commit/ci.html.haml | 65 +++++++------------ .../commit_statuses/_commit_status.html.haml | 5 +- lib/api/commit_statuses.rb | 2 +- 6 files changed, 62 insertions(+), 85 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 623ff619c4..201b6f62b8 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -106,50 +106,47 @@ module Ci end def refs - statuses.pluck(:ref).compact.uniq + statuses.order(:ref).pluck(:ref).uniq end - def statuses_for_ref(ref = nil) - if ref - statuses.for_ref(ref) - else - statuses - end + def latest_statuses + @latest_statuses ||= statuses.latest.to_a end - def builds_without_retry(ref = nil) - if ref - builds.for_ref(ref).latest - else - builds.latest - end + def builds_without_retry + @builds_without_retry ||= builds.latest.to_a + end + + def builds_without_retry_for_ref(ref) + builds_without_retry.select { |build| build.ref == ref } end def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end - def status(ref = nil) + def status if yaml_errors.present? return 'failed' end - latest_statuses = statuses.latest.to_a - latest_statuses.reject! { |status| status.try(&:allow_failure?) } - latest_statuses.select! { |status| status.ref.nil? || status.ref == ref } if ref + @status ||= begin + latest = latest_statuses + latest.reject! { |status| status.try(&:allow_failure?) } - if latest_statuses.none? - return 'skipped' - elsif latest_statuses.all?(&:success?) - 'success' - elsif latest_statuses.all?(&:pending?) - 'pending' - elsif latest_statuses.any?(&:running?) || latest_statuses.any?(&:pending?) - 'running' - elsif latest_statuses.all?(&:canceled?) - 'canceled' - else - 'failed' + 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 end @@ -173,8 +170,9 @@ module Ci status == 'canceled' end - def duration(ref = nil) - statuses_for_ref(ref).latest.select(&:duration).sum(&:duration).to_i + def duration + duration_array = latest_statuses.map(&:duration).compact + duration_array.reduce(:+).to_i end def finished_at @@ -190,8 +188,8 @@ module Ci end end - def matrix?(ref) - builds_without_retry(ref).pluck(:id).size > 1 + def matrix_for_ref?(ref) + builds_without_retry_for_ref(ref).size > 1 end def config_processor diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b6234c896e..b4d91b1b0c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -16,8 +16,9 @@ class CommitStatus < ActiveRecord::Base scope :success, -> { where(status: 'success') } scope :failed, -> { where(status: 'failed') } scope :running_or_pending, -> { where(status:[:running, :pending]) } - scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } - scope :for_ref, ->(ref) { where(ref: [ref, nil]) } + 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 diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 66e668f377..b561078e8c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -7,9 +7,9 @@ %code #{@build.ref} #up-build-trace - - if @commit.matrix?(@build.ref) + - if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu.build-top-menu - - @commit.builds_without_retry(@build.ref).each do |build| + - @commit.builds_without_retry_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do = ci_icon_for_status(build.status) @@ -20,7 +20,7 @@ = build.id - - unless @commit.builds_without_retry(@build.ref).include?(@build) + - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) %li.active %a Build ##{@build.id} diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index 7f106631ca..585f012fe0 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -20,49 +20,28 @@ .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit -- if @ci_commit.refs.blank? - .gray-content-block.second-block - Latest builds - - if @ci_commit.duration > 0 - %small.pull-right - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration} +.gray-content-block.second-block + Latest + - if @ci_commit.duration > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @ci_commit.duration} - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %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.statuses.latest, coverage: @ci_project.try(:coverage_enabled?), controls: true - -- @ci_commit.refs.sort.each do |ref| - .gray-content-block.second-block - Builds for #{ref} - - if @ci_commit.duration(ref) > 0 - %small.pull-right - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration(ref)} - - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %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.statuses.for_ref(ref).latest, coverage: @ci_project.try(:coverage_enabled?), controls: true +%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, coverage: @ci_project.try(:coverage_enabled?), controls: true - if @ci_commit.retried.any? .gray-content-block.second-block @@ -81,4 +60,4 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, coverage: @ci_project.try(:coverage_enabled?), ref: true + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, coverage: @ci_project.try(:coverage_enabled?) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 14b814bb0d..e3a17faf0b 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -9,9 +9,8 @@ - else %strong Build ##{commit_status.id} - - if defined?(ref) && ref - %td - = commit_status.ref + %td + = commit_status.ref %td = commit_status.stage diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 2a005d6a9f..50ca89079e 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -53,7 +53,7 @@ module API name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) - status = GenericCommitStatus.new(commit: ci_commit, user: current_user) unless status + status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user) status.update(attrs) case params[:state].to_s From 2e9c1608e56a20b93608ddcd569c6a45d3a229eb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 16:35:58 +0200 Subject: [PATCH 311/543] Fix commit skipping --- app/services/ci/create_commit_service.rb | 10 +++++----- spec/models/ci/project_services/mail_service_spec.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index 0ae3538757..79b0291fab 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -17,11 +17,11 @@ module Ci tag = origin_ref.start_with?('refs/tags/') commit = project.gl_project.ensure_ci_commit(sha) - return false if commit.skip_ci? - - commit.update_committed! - commit.create_builds(ref, tag, user) - + unless commit.skip_ci? + commit.update_committed! + commit.create_builds(ref, tag, user) + end + commit end end diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index 2ce5c2b470..d9b3d34ff1 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -35,7 +35,7 @@ describe Ci::MailService do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -167,7 +167,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } before do allow(mail).to receive_messages( From 5cd504ed331dd23d17709285237945fb867978a8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 16:39:08 +0200 Subject: [PATCH 312/543] Rename builds_without_retry to latest_builds --- app/models/ci/commit.rb | 14 +++++++------- app/models/project_services/ci/hip_chat_service.rb | 2 +- app/models/project_services/ci/mail_service.rb | 2 +- app/models/project_services/ci/slack_message.rb | 2 +- app/models/project_services/ci/slack_service.rb | 2 +- app/services/ci/create_commit_service.rb | 2 +- app/views/projects/builds/show.html.haml | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 201b6f62b8..296890c5e7 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -48,7 +48,7 @@ module Ci end def retry - builds_without_retry.each do |build| + latest_builds.each do |build| Ci::Build.retry(build) end end @@ -113,12 +113,12 @@ module Ci @latest_statuses ||= statuses.latest.to_a end - def builds_without_retry - @builds_without_retry ||= builds.latest.to_a + def latest_builds + @latest_builds ||= builds.latest.to_a end - def builds_without_retry_for_ref(ref) - builds_without_retry.select { |build| build.ref == ref } + def latest_builds_for_ref(ref) + latest_builds.select { |build| build.ref == ref } end def retried @@ -181,7 +181,7 @@ module Ci def coverage if project.coverage_enabled? - coverage_array = builds_without_retry.map(&:coverage).compact + coverage_array = latest_builds.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end @@ -189,7 +189,7 @@ module Ci end def matrix_for_ref?(ref) - builds_without_retry_for_ref(ref).size > 1 + latest_builds_for_ref(ref).size > 1 end def config_processor diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb index 0e6e97394b..f17993d9f3 100644 --- a/app/models/project_services/ci/hip_chat_service.rb +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -49,7 +49,7 @@ module Ci commit = build.commit return unless commit - return unless commit.builds_without_retry.include? build + return unless commit.latest_builds.include? build case commit.status.to_sym when :failed diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index 11a2743f96..fd19330100 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -48,7 +48,7 @@ module Ci # it doesn't make sense to send emails for retried builds commit = build.commit return unless commit - return unless commit.builds_without_retry.include?(build) + return unless commit.latest_builds.include?(build) case build.status.to_sym when :failed diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb index 5ac8907ecd..dc050a3fc5 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -23,7 +23,7 @@ module Ci def attachments fields = [] - commit.builds_without_retry.each do |build| + commit.latest_builds.each do |build| next if build.allow_failure? next unless build.failed? fields << { diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb index 76db573dc1..ee8e498882 100644 --- a/app/models/project_services/ci/slack_service.rb +++ b/app/models/project_services/ci/slack_service.rb @@ -48,7 +48,7 @@ module Ci commit = build.commit return unless commit - return unless commit.builds_without_retry.include?(build) + return unless commit.latest_builds.include?(build) case commit.status.to_sym when :failed diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index 79b0291fab..479a2d6def 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -21,7 +21,7 @@ module Ci commit.update_committed! commit.create_builds(ref, tag, user) end - + commit end end diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b561078e8c..9c3ae622b7 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -9,7 +9,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu.build-top-menu - - @commit.builds_without_retry_for_ref(@build.ref).each do |build| + - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do = ci_icon_for_status(build.status) @@ -20,7 +20,7 @@ = build.id - - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) + - unless @commit.latest_builds_for_ref(@build.ref).include?(@build) %li.active %a Build ##{@build.id} From c61dc1315077ad4e175a3c76991872c0e7b1d2ab Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 16:41:36 +0200 Subject: [PATCH 313/543] Fix some changes --- lib/api/entities.rb | 2 +- spec/models/commit_status_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index bfb242bb6f..519072d015 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -65,7 +65,7 @@ module API expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda { |project, options| project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url expose :star_count, :forks_count end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index cbefa7798d..c96a606fda 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -142,7 +142,7 @@ describe CommitStatus do end it 'return statuses with equal and nil ref set' do - is_expected.to eq([@commit1, @commit3]) + is_expected.to eq([@commit1]) end end From 1af4fcfa123370ab6dcf38a424c54029d7734032 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 12 Oct 2015 19:00:21 +0000 Subject: [PATCH 314/543] Fix typos in application.scss --- app/assets/stylesheets/application.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 65f775ca3f..7b060ce485 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -13,9 +13,9 @@ /* * Welcome to GitLab css! - * If you need to add or modify UI component that is commont for many pages - * like table or typography then make changes in framework/ directory. - * If you need to add uniq style that should affect only one page - use pages/ + * If you need to add or modify UI component that is common for many pages + * like a table or typography then make changes in the framework/ directory. + * If you need to add unique style that should affect only one page - use pages/ * directory. */ @@ -48,4 +48,4 @@ /* * Styles for JS behaviors. */ -@import "behaviors.scss"; +@import "behaviors.scss"; \ No newline at end of file From daca1c6511c3a09d5f0c33a8c4d29487e668afc2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 21:35:52 +0200 Subject: [PATCH 315/543] Fix broken tests --- app/models/ci/commit.rb | 4 ++-- lib/api/commit_statuses.rb | 2 +- spec/models/ci/commit_spec.rb | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 296890c5e7..f6fc464594 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -82,7 +82,7 @@ module Ci end def stage - running_or_pending = statuses.latest.running_or_pending + running_or_pending = statuses.latest.running_or_pending.ordered running_or_pending.first.try(:stage) end @@ -189,7 +189,7 @@ module Ci end def matrix_for_ref?(ref) - latest_builds_for_ref(ref).size > 1 + builds_without_retry_for_ref(ref).size > 1 end def config_processor diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 50ca89079e..2c0596c9df 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -43,7 +43,7 @@ module API # Examples: # POST /projects/:id/statuses/:sha post ':id/statuses/:sha' do - authorize! :create_commit_statuses, user_project + authorize! :create_commit_status, user_project required_attributes! [:state] attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name] commit = @project.commit(params[:sha]) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 371add4ee5..330971174f 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -125,7 +125,7 @@ describe Ci::Commit do end it 'returns all refs' do - is_expected.to contain_exactly('master', 'develop') + is_expected.to contain_exactly('master', 'develop', nil) end end @@ -225,9 +225,10 @@ describe Ci::Commit do it 'rebuilds commit' do expect(commit.status).to eq('skipped') expect(create_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) - expect(commit.status).to eq('pending') + + # since everything in Ci::Commit is cached we need to fetch a new object + new_commit = Ci::Commit.find_by_id(commit.id) + expect(new_commit.status).to eq('pending') end end end From 766da5fa7bdded8a333a758d7b172533ade5a934 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 22:34:59 +0200 Subject: [PATCH 316/543] Fix broken matrix_for_ref? Signed-off-by: Kamil Trzcinski --- app/models/ci/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index f6fc464594..68864edfbb 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -189,7 +189,7 @@ module Ci end def matrix_for_ref?(ref) - builds_without_retry_for_ref(ref).size > 1 + latest_builds_for_ref(ref).size > 1 end def config_processor From 4bf69b0bd9a27e675c98eee998f24c0122030731 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 23:59:21 +0200 Subject: [PATCH 317/543] Fix feature tests --- app/views/projects/commit/ci.html.haml | 2 +- features/steps/project/commits/commits.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index 585f012fe0..4a1ef378a3 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -21,7 +21,7 @@ \.gitlab-ci.yml not found in this commit .gray-content-block.second-block - Latest + Latest builds - if @ci_commit.duration > 0 %small.pull-right %i.fa.fa-time diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index ae5f90004e..a3cb83880e 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -118,6 +118,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'I see builds list' do expect(page).to have_content "build: pending" - expect(page).to have_content "Builds for master" + expect(page).to have_content "Latest builds" end end From 42fb52adf4460682c35d70930bcb2379ccb26171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A7=D0=B8=D0=BD=D0=B3=D0=B8=D0=B7=20=D0=90=D1=83=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D0=BE=D0=B2?= Date: Tue, 13 Oct 2015 04:41:51 +0600 Subject: [PATCH 318/543] Use css style --- app/views/dashboard/projects/_projects.html.haml | 3 ++- app/views/groups/_projects.html.haml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index 30372e9843..e641b82f81 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -5,6 +5,7 @@ - if current_user.can_create_project? %span.input-group-btn = link_to new_project_path, class: 'btn btn-green' do - + New Project + %i.fa.fa-plus + New Project = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 9c73f7a0ef..53c11a471b 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -5,6 +5,7 @@ - if can? current_user, :create_projects, @group %span.input-group-btn = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do - + New Project + %i.fa.fa-plus + New Project = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false From d02d02c672bcac0d2ef46204d132645bc69827a8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 12 Oct 2015 21:43:24 -0700 Subject: [PATCH 319/543] 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 7816488a8967d4c87e84a35027499c1a5cb5406e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 12 Oct 2015 23:02:27 -0700 Subject: [PATCH 320/543] Add "New Page" button to Wiki Pages tab Closes #2998 --- CHANGELOG | 1 + app/views/projects/wikis/pages.html.haml | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..d86f28bdc9 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) + - Add "New Page" button to Wiki Pages tab (Stan Hu) 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/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 03e6a522b2..d179a1abec 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -3,6 +3,7 @@ = render 'nav' .gray-content-block + = render 'main_links' %h3.page-title All Pages %ul.content-list From cb9b53958a4b49f4c1ab796d430674a64f1ccb9f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 13 Oct 2015 11:12:39 +0300 Subject: [PATCH 321/543] update changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index a3d796bea6..8f0bbb12d7 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) + - Add spellcheck=false to certain input fields + - Invalidate stored service password if the endpoint URL is changed v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) From 0e34154d214c0e470b7783b5086a70b4fe4cb08f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 9 Oct 2015 19:35:09 +0300 Subject: [PATCH 322/543] Project names are not fully shown if group name is too big, even on group page view --- CHANGELOG | 1 + app/views/groups/_projects.html.haml | 2 +- app/views/shared/projects/_list.html.haml | 3 ++- app/views/shared/projects/_project.html.haml | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8f0bbb12d7..063b85d0eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.1.0 (unreleased) - Persist filters when sorting on admin user page (Jerry Lukins) - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed + - Project names are not fully shown if group name is too big, even on group page view 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/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 76da3276e4..133f3e2d5a 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -7,4 +7,4 @@ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do New project - = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false + = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 16e1d8421d..357cfd6a37 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -2,11 +2,12 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - ci = false unless local_assigns[:ci] == true +- skip_namespace = false unless local_assigns[:skip_namespace] == true %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil - = render "shared/projects/project", project: project, + = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci - if projects.size > projects_limit diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e67e5a8a63..aee839b44e 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,6 +1,7 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - ci = false unless local_assigns[:ci] == true +- skip_namespace = false unless local_assigns[:skip_namespace] == true - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? %li.project-row{ class: css_class } @@ -11,7 +12,7 @@ = project_icon(project, alt: '', class: 'avatar project-avatar s46') %span.project-full-name %span.namespace-name - - if project.namespace + - if project.namespace && !skip_namespace = project.namespace.human_name \/ %span.project-name.filter-title From e4a9447451bfccbea6943bb5546b03c81321eb77 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 10:55:06 +0200 Subject: [PATCH 323/543] Rename bootstrap css file and refactor typography css Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework.scss | 5 +- app/assets/stylesheets/framework/mixins.scss | 141 ----------------- app/assets/stylesheets/framework/tables.scss | 16 ++ .../{gl_bootstrap.scss => tw_bootstrap.scss} | 20 --- ...ables.scss => tw_bootstrap_variables.scss} | 0 .../stylesheets/framework/typography.scss | 145 +++++++++++++++++- 6 files changed, 162 insertions(+), 165 deletions(-) rename app/assets/stylesheets/framework/{gl_bootstrap.scss => tw_bootstrap.scss} (93%) rename app/assets/stylesheets/framework/{gl_variables.scss => tw_bootstrap_variables.scss} (100%) diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c5e23c1c32..1ec9d2fd84 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -2,8 +2,9 @@ @import "framework/variables"; @import "framework/mixins"; @import "framework/layout"; -@import 'framework/gl_variables'; -@import 'framework/gl_bootstrap'; +@import 'framework/tw_bootstrap_variables'; +@import 'framework/tw_bootstrap'; + @import "framework/avatar.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index c74a6d3982..089e6958ee 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -54,147 +54,6 @@ @include box-shadow(0 0 0 3px #f1f1f1); } -@mixin md-typography { - color: $md-text-color; - - a { - color: $md-link-color; - } - - img { - max-width: 100%; - } - - *:first-child { - margin-top: 0; - } - - code { - font-family: $monospace_font; - white-space: pre; - word-wrap: normal; - padding: 1px 2px; - } - - kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #555; - vertical-align: middle; - background-color: #FCFCFC; - border-width: 1px; - border-style: solid; - border-color: #CCC #CCC #BBB; - border-image: none; - border-radius: 3px; - box-shadow: 0px -1px 0px #BBB inset; - } - - h1 { - font-size: 1.3em; - font-weight: 600; - margin: 24px 0 12px 0; - padding: 0 0 10px 0; - border-bottom: 1px solid #e7e9ed; - color: #313236; - } - - h2 { - font-size: 1.2em; - font-weight: 600; - margin: 24px 0 12px 0; - color: #313236; - } - - h3 { - margin: 24px 0 12px 0; - font-size: 1.25em; - } - - h4 { - margin: 24px 0 12px 0; - font-size: 1.1em; - } - - h5 { - margin: 24px 0 12px 0; - font-size: 1em; - } - - h6 { - margin: 24px 0 12px 0; - font-size: 0.90em; - } - - blockquote { - padding: 8px 21px; - margin: 12px 0 12px; - border-left: 3px solid #e7e9ed; - } - - blockquote p { - color: #7f8fa4 !important; - font-size: 15px; - line-height: 1.5; - } - - p { - color:#5c5d5e; - margin:6px 0 0 0; - } - - table { - @extend .table; - @extend .table-bordered; - margin: 12px 0 12px 0; - color: #5c5d5e; - th { - background: #f8fafc; - } - } - - pre { - margin: 12px 0 12px 0 !important; - background-color: #f8fafc !important; - font-size: 13px !important; - color: #5b6169 !important; - line-height: 1.6em !important; - @include border-radius(2px); - } - - p > code { - font-weight: inherit; - } - - - ul { - color: #5c5d5e; - } - - li { - line-height: 1.6em; - } - - a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { - &:before { - margin-right: 4px; - - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - content: "\f0c6"; - } - - &:hover:before { - text-decoration: none; - } - } -} - - @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a66e45577d..76163b3a05 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,5 +1,21 @@ table { &.table { + .dropdown-menu a { + text-decoration: none; + } + + .success, + .warning, + .danger, + .info { + color: #fff; + + a:not(.btn) { + text-decoration: underline; + color: #fff; + } + } + tr { td, th { padding: 8px 10px; diff --git a/app/assets/stylesheets/framework/gl_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss similarity index 93% rename from app/assets/stylesheets/framework/gl_bootstrap.scss rename to app/assets/stylesheets/framework/tw_bootstrap.scss index eb8d23d645..722b44d2d1 100644 --- a/app/assets/stylesheets/framework/gl_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -251,23 +251,3 @@ .text-info:hover { color: $brand-info; } - -// Tables ===================================================================== - -table.table { - .dropdown-menu a { - text-decoration: none; - } - - .success, - .warning, - .danger, - .info { - color: #fff; - - a:not(.btn) { - text-decoration: underline; - color: #fff; - } - } -} diff --git a/app/assets/stylesheets/framework/gl_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss similarity index 100% rename from app/assets/stylesheets/framework/gl_variables.scss rename to app/assets/stylesheets/framework/tw_bootstrap_variables.scss diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6a3cb49baa..bf36f96cc9 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,3 +1,144 @@ +@mixin md-typography { + color: $md-text-color; + + a { + color: $md-link-color; + } + + img { + max-width: 100%; + } + + *:first-child { + margin-top: 0; + } + + code { + font-family: $monospace_font; + white-space: pre; + word-wrap: normal; + padding: 1px 2px; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #FCFCFC; + border-width: 1px; + border-style: solid; + border-color: #CCC #CCC #BBB; + border-image: none; + border-radius: 3px; + box-shadow: 0px -1px 0px #BBB inset; + } + + h1 { + font-size: 1.3em; + font-weight: 600; + margin: 24px 0 12px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #e7e9ed; + color: #313236; + } + + h2 { + font-size: 1.2em; + font-weight: 600; + margin: 24px 0 12px 0; + color: #313236; + } + + h3 { + margin: 24px 0 12px 0; + font-size: 1.25em; + } + + h4 { + margin: 24px 0 12px 0; + font-size: 1.1em; + } + + h5 { + margin: 24px 0 12px 0; + font-size: 1em; + } + + h6 { + margin: 24px 0 12px 0; + font-size: 0.90em; + } + + blockquote { + padding: 8px 21px; + margin: 12px 0 12px; + border-left: 3px solid #e7e9ed; + } + + blockquote p { + color: #7f8fa4 !important; + font-size: 15px; + line-height: 1.5; + } + + p { + color:#5c5d5e; + margin:6px 0 0 0; + } + + table { + @extend .table; + @extend .table-bordered; + margin: 12px 0 12px 0; + color: #5c5d5e; + th { + background: #f8fafc; + } + } + + pre { + margin: 12px 0 12px 0 !important; + background-color: #f8fafc !important; + font-size: 13px !important; + color: #5b6169 !important; + line-height: 1.6em !important; + @include border-radius(2px); + } + + p > code { + font-weight: inherit; + } + + + ul { + color: #5c5d5e; + } + + li { + line-height: 1.6em; + } + + a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } +} + + /** * Headers * @@ -6,7 +147,7 @@ body { text-rendering:optimizeLegibility; -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; } - + .page-title { margin-top: 0px; line-height: 1.3; @@ -127,4 +268,4 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; -} \ No newline at end of file +} From 029edcc391d492c30f0598e70dba3433189806c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 11:02:44 +0200 Subject: [PATCH 324/543] Fix padding for controls in list Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 3bfed8de77..fdfbb88692 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: 10px; + padding-top: 5px; float: right; } } From f1e3894973abd96a318b5f3f56bfce110c620c39 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 11:23:10 +0200 Subject: [PATCH 325/543] Remove unused twbs components Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/tw_bootstrap.scss | 2 -- app/assets/stylesheets/pages/note_form.scss | 1 - 2 files changed, 3 deletions(-) diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 722b44d2d1..99d028d122 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -32,8 +32,6 @@ @import "bootstrap/pager"; @import "bootstrap/labels"; @import "bootstrap/badges"; -@import "bootstrap/jumbotron"; -@import "bootstrap/thumbnails"; @import "bootstrap/alerts"; @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index dcd1aed719..4392f08942 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -65,7 +65,6 @@ .note-image-attach { @extend .col-md-4; - @extend .thumbnail; margin-left: 45px; float: none; } From ab604c73ef986d03d5c5c04075663d87ef390479 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 11:33:46 +0200 Subject: [PATCH 326/543] Temporary return sm and xs button sizes Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/buttons.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 11acbe3adf..e5f0c0ad9e 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -6,7 +6,7 @@ font-size: 13px; font-weight: 600; line-height: 18px; - padding: 11px 16px; + padding: 11px $gl-padding; letter-spacing: .4px; &:focus, @@ -71,6 +71,14 @@ @include btn-default; @include btn-white; + &.btn-sm { + padding: 5px 10px; + } + + &.btn-xs { + padding: 1px 5px; + } + &.btn-success, &.btn-new, &.btn-create, From 127f288afbc454975bee910cee22f330a86c0e1b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 09:37:42 +0000 Subject: [PATCH 327/543] 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 e7cc554cc181cbb850f89af26e64a9ab56116f28 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 13 Oct 2015 11:50:37 +0200 Subject: [PATCH 328/543] Fix retry and cancel URLs --- app/models/ci/build.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cb6a101521..f8c731a7bf 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -219,13 +219,15 @@ module Ci def cancel_url if active? - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + Gitlab::Application.routes.url_helpers. + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) end end def retry_url if commands.present? - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + Gitlab::Application.routes.url_helpers. + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) end end From df99ddbba13db4a7699bf1d585675f921cf382ce Mon Sep 17 00:00:00 2001 From: Han Loong Liauw Date: Tue, 13 Oct 2015 21:24:44 +1100 Subject: [PATCH 329/543] 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 4ad61519f9f9cdc82b1ee6bd1ed92905692c7e7f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 13:02:27 +0200 Subject: [PATCH 330/543] Fix control buttons in lists Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index fdfbb88692..c5764c3659 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -117,8 +117,12 @@ ul.content-list { } .controls { - padding-top: 5px; + padding-top: 4px; float: right; + + .btn { + padding: 10px 14px; + } } } } From 219451241a92ff9e81dcb77914e6d659ea2f7376 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 13:03:27 +0200 Subject: [PATCH 331/543] Fix UI issue on project page with no ssh key message Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/_page.html.haml | 3 ++- app/views/projects/show.html.haml | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 2468687b56..1a883e20e8 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,7 +6,7 @@ = brand_header_logo .gitlab-text-container %h3 GitLab - + - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user @@ -23,6 +23,7 @@ = current_user.username .content-wrapper = render "layouts/flash" + = yield :flash_message %div{ class: container_class } .content .clearfix diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index efa119edd5..e95d987d74 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -2,9 +2,10 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") -- if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += content_for :flash_message do + - if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render 'shared/no_password' - if prefer_readme? = render 'projects/last_push' From 712d17684b2b9a8664cdff685c44fa59ea6fabbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 13:07:59 +0200 Subject: [PATCH 332/543] 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 333/543] 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 e75655950d27eff75dba0aadd2f853f84034bb3a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 13:37:15 +0200 Subject: [PATCH 334/543] Several fixes for UI on mobile Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/mobile.scss | 14 +++++++++++--- app/assets/stylesheets/pages/projects.scss | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 36ae126f86..cea47fba19 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,7 +23,7 @@ margin-right: 0; } - .issues-filters, + .issues-details-filters, .dash-projects-filters, .check-all-holder { display: none; @@ -83,6 +83,7 @@ .center-top-menu { height: 45px; + margin-bottom: 30px; li a { font-size: 14px; @@ -90,9 +91,11 @@ } } - .projects-search-form { - margin: 0 -5px !important; + .activity-filter-block { + display: none; + } + .projects-search-form { .btn { display: none; } @@ -100,6 +103,11 @@ } @media (max-width: $screen-sm-max) { + .page-with-sidebar .content-wrapper { + padding: 0; + padding-top: 1px; + } + .issues-filters { .milestone-filter, .labels-filter { display: none; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0031ab5151..9d42c0f28e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -63,6 +63,7 @@ } p { + padding: 0 $gl-padding; color: #5c5d5e; } } From 2f68fb9cc3622eb90c955b6936e91ea766f262b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 14:38:38 +0200 Subject: [PATCH 335/543] Small css cleanup Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/common.scss | 4 ---- app/assets/stylesheets/pages/projects.scss | 5 ----- app/views/dashboard/_activities.html.haml | 7 +++---- app/views/groups/show.html.haml | 8 +++----- app/views/projects/_activity.html.haml | 7 +++---- app/views/projects/buttons/_notifications.html.haml | 2 +- app/views/projects/new.html.haml | 4 ++-- 7 files changed, 12 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 03919f15f1..e1a1793be9 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -398,7 +398,3 @@ table { .space-right { margin-right: 10px; } - -.in-line { - display: inline-block; -} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9d42c0f28e..f7a2284900 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -511,8 +511,3 @@ pre.light-well { margin-top: -1px; } } - -.inline-form { - display: inline-block; -} - diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 19d919f9b6..f98fd9f06b 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -3,10 +3,9 @@ .gray-content-block - if current_user - %ul.nav.nav-pills.event_filter.pull-right - %li.pull-right - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index a9ba9d2ba1..dc8e81323a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -25,11 +25,9 @@ .hidden-xs - if current_user = render "events/event_last_push", event: @last_push - - %ul.nav.nav-pills.event_filter.pull-right - %li - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' %hr diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 1261f6254d..c2683bc621 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,10 +1,9 @@ = render 'projects/last_push' .gray-content-block.activity-filter-block - if current_user - %ul.nav.nav-pills.event_filter.pull-right - %li - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list{:"data-href" => activity_project_path(@project)} diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 4b69a6d7a6..3bc2daeec4 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,6 +1,6 @@ - return unless @membership -= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do += 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 = hidden_field_tag :notification_level diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 1b093c8f51..daab2326bc 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -111,10 +111,10 @@ - if current_user.can_create_group? .pull-right - .light.in-line + .light.inline .space-right Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-xs" do + = link_to new_group_path, class: "btn" do Create a group .save-project-loader.hide From a0a488ed13e2a07e0dd42e8795c50ea762917f20 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 13 Oct 2015 16:41:48 +0200 Subject: [PATCH 336/543] Apply new design to files page Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + .../javascripts/line_highlighter.js.coffee | 6 +- app/assets/stylesheets/framework/tables.scss | 2 +- .../framework/tw_bootstrap_variables.scss | 2 + .../stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/tree.scss | 34 +++--- app/views/projects/blob/_blob.html.haml | 2 +- .../repositories/_download_archive.html.haml | 4 +- app/views/projects/tree/_readme.html.haml | 13 ++- app/views/projects/tree/_tree.html.haml | 106 +++++++++--------- .../fixtures/line_highlighter.html.haml | 2 +- .../line_highlighter_spec.js.coffee | 2 +- 12 files changed, 87 insertions(+), 88 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8f0bbb12d7..8b3df158a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.1.0 (unreleased) - Persist filters when sorting on admin user page (Jerry Lukins) - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed + - Apply new design for Files page 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/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee index e604e6025c..2254a3f91a 100644 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -6,7 +6,7 @@ # # ### Example Markup # -#
+#
#
#
# 1 @@ -53,7 +53,7 @@ class @LineHighlighter $.scrollTo("#L#{range[0]}", offset: -150) bindEvents: -> - $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler # While it may seem odd to bind to the mousedown event and then throw away # the click event, there is a method to our madness. @@ -62,7 +62,7 @@ class @LineHighlighter # active state even when the event is cancelled, resulting in an ugly border # around the link and/or a persisted underline text decoration. - $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) -> event.preventDefault() clickHandler: (event) => diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 76163b3a05..789b34020c 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -28,7 +28,7 @@ table { border-bottom: 1px solid $border-color !important; } td { - border-color: #F1F1F1 !important; + border-color: $table-border-color !important; border-bottom: 1px solid; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 18632da4f2..63868a34e2 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -156,3 +156,5 @@ $nav-link-padding: 13px $gl-padding; $pre-bg: #f8fafc !default; $pre-color: $gl-gray !default; $pre-border-color: #e7e9ed; + +$table-bg-accent: $background-color; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index eb9a296638..91954683c3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -16,6 +16,7 @@ $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; $border-color: #dce0e6; +$table-border-color: #eef0f2; $background-color: #F7F8FA; $header-height: 58px; $fixed-layout-width: 1200px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 271cc547e2..dadd86e88c 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,7 +1,7 @@ .tree-holder { - .tree-content-holder { - float: left; - width: 100%; + .tree-table-holder { + margin-left: -$gl-padding; + margin-right: -$gl-padding; } .tree_progress { @@ -13,10 +13,15 @@ } .tree-table { - @extend .table; - @include border-radius(0); + margin-bottom: 0; tr { + > td, > th { + padding: 10px $gl-padding; + line-height: 32px; + border-color: $table-border-color !important; + } + &:hover { td { background: $hover; @@ -27,9 +32,9 @@ } &.selected { td { - background: $background-color; - border-top: 1px solid #EEE; - border-bottom: 1px solid #EEE; + background: $gray-dark; + border-top: 1px solid $border-gray-dark; + border-bottom: 1px solid $border-gray-dark; } } } @@ -85,19 +90,6 @@ margin-right: 15px; } -.readme-holder { - margin: 0 auto; - - .readme-file-title { - font-size: 14px; - font-weight: bold; - margin-bottom: 20px; - color: #777; - border-bottom: 1px solid #DDD; - padding: 10px 0; - } -} - .blob-commit-info { list-style: none; margin: 0; diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index b4c7d8b9b7..a1ae139758 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -19,7 +19,7 @@ - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project -%div#tree-content-holder.tree-content-holder +%div#blob-content-holder.blob-content-holder %article.file-holder .file-title = blob_icon blob.mode, blob.name diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index b9486a9b49..07c24950ee 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-success col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index f082d71186..7e9af19c8b 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,7 +1,8 @@ -%article.readme-holder#README - = link_to '#README' do - %h4.readme-file-title - %i.fa.fa-file - = readme.name - .wiki +%article.file-holder.readme-holder#README + .file-title + = link_to '#README' do + %strong + %i.fa.fa-file + = readme.name + .file-content.wiki = render_readme(readme) diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 457f8a4a58..7ff48e32e6 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -1,59 +1,61 @@ -%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| +.gray-content-block + %ul.breadcrumb.repo-breadcrumb %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.btn-xs.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 + = 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.prepend-top-20 - %table#tree-slider{class: "table_#{@hex_path} tree-table" } - %thead - %tr - %th Name - %th Last Update - %th.hidden-xs - .pull-left Last Commit - .last-commit.hidden-sm.pull-left -   - %i.fa.fa-angle-right -   - %small.light - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) - – - = truncate(@commit.title, length: 50) - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' +%div#tree-content-holder.tree-content-holder + .tree-table-holder + %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } + %thead + %tr + %th Name + %th Last Update + %th.hidden-xs + .pull-left Last Commit + .last-commit.hidden-sm.pull-left +   + %i.fa.fa-angle-right +   + %small.light + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) + – + = truncate(@commit.title, length: 50) + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - - if @path.present? - %tr.tree-item - %td.tree-item-file-name - = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' - %td - %td.hidden-xs + - if @path.present? + %tr.tree-item + %td.tree-item-file-name + = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' + %td + %td.hidden-xs - = render_tree(tree) + = render_tree(tree) - if tree.readme = render "projects/tree/readme", readme: tree.readme diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml index da1ebcdb23..514877340e 100644 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -1,4 +1,4 @@ -#tree-content-holder +#blob-content-holder .file-content .line-numbers - 1.upto(25) do |i| diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee index 57453c716a..a073f21e7b 100644 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -39,7 +39,7 @@ describe 'LineHighlighter', -> expect(spy).toHaveBeenPrevented() it 'handles garbage input from the hash', -> - func = -> new LineHighlighter('#tree-content-holder') + func = -> new LineHighlighter('#blob-content-holder') expect(func).not.toThrow() describe '#clickHandler', -> From a092d27025cbaf4abe8a1198a25c5becfc07c9b1 Mon Sep 17 00:00:00 2001 From: sue445 Date: Wed, 14 Oct 2015 00:30:48 +0900 Subject: [PATCH 337/543] [ci skip] Fix wrong comment According to `attributes_for_keys` and API doc, `POST /projects/:id/merge_requests` is received `target_project_id` (NOT `target_project`) --- lib/api/merge_requests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 63ea2f0543..f3a59fadf2 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -99,7 +99,7 @@ module API # id (required) - The ID of a project - this will be the source of the merge request # source_branch (required) - The source branch # target_branch (required) - The target branch - # target_project - The target project of the merge request defaults to the :id of the project + # target_project_id - The target project of the merge request defaults to the :id of the project # assignee_id - Assignee user ID # title (required) - Title of MR # description - Description of MR From 2297a7ba1f5d004c877a7cb82510d7d635f90ec0 Mon Sep 17 00:00:00 2001 From: sue445 Date: Wed, 14 Oct 2015 00:40:37 +0900 Subject: [PATCH 338/543] [ci skip] Add missing parameters in API doc --- doc/api/merge_requests.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index bb551fc67f..ffa7f2cdf1 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -188,6 +188,7 @@ Parameters: - `title` (required) - Title of MR - `description` (optional) - Description of MR - `target_project_id` (optional) - The target project (numeric id) +- `labels` (optional) - Labels for MR as a comma-separated list ```json { @@ -239,6 +240,7 @@ Parameters: - `title` - Title of MR - `description` - Description of MR - `state_event` - New state (close|reopen|merge) +- `labels` (optional) - Labels for MR as a comma-separated list ```json { From 251e13666d04a1c8427401962e3e171e569d9088 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 18:23:49 +0200 Subject: [PATCH 339/543] 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 135ec3242d803fc46087b59d10016e55207f6743 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 18:27:25 +0200 Subject: [PATCH 340/543] Reduce font size of commit references to not stand out as much --- app/assets/stylesheets/generic/gfm.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss index bd9200ace2..5ae0520fd7 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/generic/gfm.scss @@ -22,4 +22,5 @@ .gfm-commit, .gfm-commit_range { font-family: $monospace_font; + font-size: 90%; } From 8346dde0520ed625446ecc5d5a35b53e0b60dbb0 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 9 Oct 2015 20:07:29 +0300 Subject: [PATCH 341/543] Only render 404 page from /public --- CHANGELOG | 1 + app/controllers/application_controller.rb | 6 +----- app/controllers/import/bitbucket_controller.rb | 2 +- app/controllers/import/fogbugz_controller.rb | 2 +- app/controllers/import/github_controller.rb | 2 +- app/controllers/import/gitlab_controller.rb | 2 +- app/controllers/import/gitorious_controller.rb | 2 +- app/controllers/import/google_code_controller.rb | 2 +- app/controllers/projects/avatars_controller.rb | 2 +- app/controllers/projects/blob_controller.rb | 6 +++--- app/controllers/projects/raw_controller.rb | 2 +- app/controllers/projects/tree_controller.rb | 6 +++--- app/controllers/projects/uploads_controller.rb | 2 +- app/controllers/uploads_controller.rb | 6 +++--- lib/extracts_path.rb | 2 +- 15 files changed, 21 insertions(+), 24 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8f0bbb12d7..18fc76e444 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.1.0 (unreleased) - Persist filters when sorting on admin user page (Jerry Lukins) - Add spellcheck=false to certain input fields - Invalidate stored service password if the endpoint URL is changed + - Only render 404 page from /public 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/application_controller.rb b/app/controllers/application_controller.rb index 527c9da0fa..2b2ea3dff1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound do |exception| log_exception(exception) - render "errors/not_found", layout: "errors", status: 404 + render_404 end protected @@ -149,10 +149,6 @@ class ApplicationController < ActionController::Base render "errors/access_denied", layout: "errors", status: 404 end - def not_found! - render "errors/not_found", layout: "errors", status: 404 - end - def git_not_found! render "errors/git_not_found", layout: "errors", status: 404 end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index f84f85a7df..25e5872486 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -62,7 +62,7 @@ class Import::BitbucketController < Import::BaseController end def verify_bitbucket_import_enabled - not_found! unless bitbucket_import_enabled? + render_404 unless bitbucket_import_enabled? end def bitbucket_auth diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 849646cd66..1830039085 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -99,6 +99,6 @@ class Import::FogbugzController < Import::BaseController end def verify_fogbugz_import_enabled - not_found! unless fogbugz_import_enabled? + render_404 unless fogbugz_import_enabled? end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index f21fbd9ecc..aae77d384c 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -47,7 +47,7 @@ class Import::GithubController < Import::BaseController end def verify_github_import_enabled - not_found! unless github_import_enabled? + render_404 unless github_import_enabled? end def github_auth diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 27af19f5f6..23a396e808 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -44,7 +44,7 @@ class Import::GitlabController < Import::BaseController end def verify_gitlab_import_enabled - not_found! unless gitlab_import_enabled? + render_404 unless gitlab_import_enabled? end def gitlab_auth diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index f24cdb3709..eecbe380c9 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -42,7 +42,7 @@ class Import::GitoriousController < Import::BaseController end def verify_gitorious_import_enabled - not_found! unless gitorious_import_enabled? + render_404 unless gitorious_import_enabled? end end diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 82fadeb7e8..41472a6fe6 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -106,7 +106,7 @@ class Import::GoogleCodeController < Import::BaseController end def verify_google_code_import_enabled - not_found! unless google_code_import_enabled? + render_404 unless google_code_import_enabled? end def user_map diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 9c3763d593..548f1b9ebf 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -12,7 +12,7 @@ class Projects::AvatarsController < Projects::ApplicationController filename: @blob.name ) else - not_found! + render_404 end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ae9b138446..8cc2f21d88 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -113,14 +113,14 @@ class Projects::BlobController < Projects::ApplicationController end end - return not_found! + return render_404 end end def commit @commit = @repository.commit(@ref) - return not_found! unless @commit + return render_404 unless @commit end def assign_blob_vars @@ -128,7 +128,7 @@ class Projects::BlobController < Projects::ApplicationController @ref, @path = extract_ref(@id) rescue InvalidPathError - not_found! + render_404 end def after_edit_path diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 5f6fbce795..d5ee6ac866 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -20,7 +20,7 @@ class Projects::RawController < Projects::ApplicationController disposition: 'inline' ) else - not_found! + render_404 end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 7eaff1d61e..bdcb1a3e29 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -10,7 +10,7 @@ class Projects::TreeController < Projects::ApplicationController before_action :authorize_push_code!, only: [:create_dir] def show - return not_found! unless @repository.commit(@ref) + return render_404 unless @repository.commit(@ref) if tree.entries.empty? if @repository.blob_at(@commit.id, @path) @@ -19,7 +19,7 @@ class Projects::TreeController < Projects::ApplicationController File.join(@ref, @path)) ) and return elsif @path.present? - return not_found! + return render_404 end end @@ -31,7 +31,7 @@ class Projects::TreeController < Projects::ApplicationController end def create_dir - return not_found! unless @commit_params.values.all? + return render_404 unless @commit_params.values.all? begin result = Files::CreateDirService.new(@project, current_user, @commit_params).execute diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 71ecc20dd9..e1fe7ea211 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -20,7 +20,7 @@ class Projects::UploadsController < Projects::ApplicationController end def show - return not_found! if uploader.nil? || !uploader.file.exists? + return render_404 if uploader.nil? || !uploader.file.exists? disposition = uploader.image? ? 'inline' : 'attachment' send_file uploader.file.path, disposition: disposition diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 28536e359e..868b05929d 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -10,7 +10,7 @@ class UploadsController < ApplicationController end unless uploader.file && uploader.file.exists? - return not_found! + return render_404 end disposition = uploader.image? ? 'inline' : 'attachment' @@ -21,7 +21,7 @@ class UploadsController < ApplicationController def find_model unless upload_model && upload_mount - return not_found! + return render_404 end @model = upload_model.find(params[:id]) @@ -44,7 +44,7 @@ class UploadsController < ApplicationController return if authorized if current_user - not_found! + render_404 else authenticate_user! end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 322aed5e27..51e46da82c 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -110,7 +110,7 @@ module ExtractsPath @project, @ref, @path) rescue RuntimeError, NoMethodError, InvalidPathError - not_found! + render_404 end def tree From 93fcddd7a7ce4ed259794a4511ae04035ae33be2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 13 Oct 2015 22:53:05 +0200 Subject: [PATCH 342/543] 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 343/543] 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 344/543] 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 345/543] 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 346/543] 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 347/543] 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 348/543] 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 d313a7681be1f307d8178eca13a2e73118f80930 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 09:46:06 +0200 Subject: [PATCH 349/543] 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 350/543] 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 351/543] 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 352/543] 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 353/543] 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 354/543] 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 355/543] 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 356/543] 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 33c9d6e45c0800fd5a312af2c286826764fd7589 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 13 Oct 2015 16:51:13 +0200 Subject: [PATCH 357/543] Fix retry and cancel for build --- app/models/ci/build.rb | 4 ++-- app/views/projects/commit/ci.html.haml | 22 +++++++++++-------- .../commit_statuses/_commit_status.html.haml | 6 ++--- spec/features/commits_spec.rb | 10 ++++++++- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f8c731a7bf..cfafbf6786 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -220,14 +220,14 @@ module Ci def cancel_url if active? Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self) end end def retry_url if commands.present? Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url) + retry_namespace_project_build_path(gl_project.namespace, gl_project, self) end end diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index 4a1ef378a3..ca71a91af1 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -3,11 +3,6 @@ = render "commit_box" = render "ci_menu" -- if @ci_project && current_user && can?(current_user, :manage_builds, @project) - .pull-right - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-sm btn-danger' - - if @ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger @@ -22,11 +17,18 @@ .gray-content-block.second-block Latest builds - - if @ci_commit.duration > 0 - %small.pull-right + + .pull-right + - if @ci_commit.duration > 0 %i.fa.fa-time #{time_interval_in_words @ci_commit.duration} +   + + - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + - 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 @@ -41,7 +43,8 @@ %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, coverage: @ci_project.try(:coverage_enabled?), controls: true + = 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 @@ -60,4 +63,5 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, coverage: @ci_project.try(:coverage_enabled?) + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_project.try(:coverage_enabled?) } diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index e3a17faf0b..7314f8e79d 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -41,11 +41,11 @@ #{commit_status.coverage}% %td - - if defined?(controls) && controls && current_user && can?(current_user, :manage_builds, gl_project) - .pull-right + .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 - - elsif commit_status.retry_url + - 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/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index cbb6360069..1adc2cdf70 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -29,8 +29,16 @@ describe "Commits" do it { expect(page).to have_content @commit.git_author_name } end - describe "Cancel commit" do + describe "Cancel all builds" do it "cancels commit" do + visit ci_status_path(@commit) + click_on "Cancel all" + expect(page).to have_content "canceled" + end + end + + describe "Cancel build" do + it "cancels build" do visit ci_status_path(@commit) click_on "Cancel" expect(page).to have_content "canceled" From 61d8f9617681973f04d264762b54840d44dea02a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 14 Oct 2015 13:46:17 +0200 Subject: [PATCH 358/543] 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 359/543] 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 360/543] 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 361/543] 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 362/543] 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 363/543] 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 2d0fcb4de23ab368e2e030b3cf7f6b1705ef676f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 14 Oct 2015 17:26:24 +0200 Subject: [PATCH 364/543] Fix spinach tests introduced by 07101cfab61f28c6328efebea98f018ab8356cdd --- features/dashboard/new_project.feature | 10 +++++----- features/steps/dashboard/new_project.rb | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index bbd82a85e3..7639206835 100644 --- a/features/dashboard/new_project.feature +++ b/features/dashboard/new_project.feature @@ -7,24 +7,24 @@ Background: And I click "New project" link @javascript - Scenario: I should see New projects page - Then I see "New project" page + Scenario: I should see New Projects page + Then I see "New Project" page Then I see all possible import optios @javascript Scenario: I should see instructions on how to import from Git URL - Given I see "New project" page + Given I see "New Project" page When I click on "Any repo by URL" Then I see instructions on how to import from Git URL @javascript Scenario: I should see instructions on how to import from GitHub - Given I see "New project" page + Given I see "New Project" page When I click on "Import project from GitHub" Then I see instructions on how to import from GitHub @javascript Scenario: I should see Google Code import page - Given I see "New project" page + Given I see "New Project" page When I click on "Google Code" Then I redirected to Google Code import page diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 1e09162a5b..44a4aa9844 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -3,13 +3,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedPaths include SharedProject - step 'I click "New project" link' do + step 'I click "New Project" link' do page.within('.content') do - click_link "New project" + click_link "New Project" end end - step 'I see "New project" page' do + step 'I see "New Project" page' do expect(page).to have_content('Project path') end From 7b5ab3ded5e6f5d88f04802d5a71a9ae94d20f92 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 12 Oct 2015 17:06:53 +0200 Subject: [PATCH 365/543] 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 366/543] 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 367/543] 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 368/543] 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 369/543] 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 370/543] 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 371/543] 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 372/543] 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 373/543] 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 374/543] 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 375/543] 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 376/543] 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 377/543] 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 378/543] 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 379/543] 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 380/543] 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 381/543] 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 382/543] 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 383/543] 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 384/543] 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 385/543] 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 386/543] 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 387/543] 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 388/543] 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 389/543] 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 390/543] 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 391/543] 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 392/543] 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 393/543] 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 394/543] 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 395/543] 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 396/543] 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 397/543] 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 398/543] 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 399/543] 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 400/543] 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 401/543] 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 402/543] 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 403/543] 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 404/543] 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 405/543] 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 406/543] 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 407/543] 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 408/543] 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 409/543] 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 410/543] 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 411/543] 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 412/543] 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 413/543] 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 414/543] 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 415/543] 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 416/543] 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 417/543] 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 418/543] 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 419/543] 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 420/543] 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 421/543] 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 422/543] 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 423/543] 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 424/543] 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 425/543] 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 426/543] 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 427/543] 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 428/543] 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 429/543] 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 430/543] 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 431/543] 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 432/543] 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 433/543] 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 434/543] 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 435/543] 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 436/543] 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 437/543] 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 438/543] 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 439/543] 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 440/543] 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 441/543] 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 442/543] 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 443/543] 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 444/543] 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 445/543] 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 446/543] 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 447/543] 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 448/543] 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 449/543] 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 450/543] 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 451/543] 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 452/543] 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 f6664601719925834fbf6784545ee64ad9413a12 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 16 Oct 2015 15:18:21 +0000 Subject: [PATCH 453/543] 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 454/543] 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 455/543] 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 456/543] 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 457/543] 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 458/543] 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 459/543] 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 460/543] 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 461/543] 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 462/543] 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 463/543] 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 464/543] 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 465/543] 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 466/543] 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 467/543] 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 468/543] 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 469/543] 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 470/543] 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 471/543] 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 472/543] 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 473/543] 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 474/543] 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 475/543] 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 476/543] 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 477/543] 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 478/543] 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 479/543] 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 480/543] 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 481/543] 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 482/543] 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 483/543] 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 484/543] 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 485/543] 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 486/543] 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 487/543] 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 488/543] 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 489/543] 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 490/543] 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 491/543] 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 492/543] 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 493/543] 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 494/543] 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 495/543] 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 496/543] 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 497/543] 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 498/543] 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 499/543] 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 500/543] 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 501/543] 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 502/543] 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 503/543] 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 504/543] 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 505/543] 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 506/543] 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 507/543] 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 508/543] 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 509/543] 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 510/543] 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 511/543] 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 512/543] 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 513/543] 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 514/543] 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 515/543] 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 516/543] 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 517/543] 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 518/543] 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 519/543] 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 520/543] 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 521/543] 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 522/543] 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 523/543] 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 c21d46c29ffe2547cb454c2658fa9644a19f1dd8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Oct 2015 12:34:09 +0200 Subject: [PATCH 524/543] 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 525/543] 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 4eac37fd872db5e1b73d3d74b943a943ae7e1d6a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Oct 2015 23:33:49 -0700 Subject: [PATCH 526/543] 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 527/543] 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 528/543] 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 529/543] 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 530/543] 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 531/543] 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 532/543] 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 533/543] 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 534/543] 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 535/543] 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 536/543] 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 537/543] 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 5f1c99b757b7c23d476b6b523160c242db09259f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 22 Oct 2015 16:08:45 +0200 Subject: [PATCH 538/543] 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 539/543] 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 540/543] 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 541/543] 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 542/543] 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 543/543] 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')

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 literal 150244 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 dL#lX9xsrRY)+ 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